diff --git a/examples/streamed/server/server-cli.inlinesourcemap.js b/examples/streamed/server/server-cli.inlinesourcemap.js index fdac3bfe..d1966fc9 100644 --- a/examples/streamed/server/server-cli.inlinesourcemap.js +++ b/examples/streamed/server/server-cli.inlinesourcemap.js @@ -1701,9 +1701,10 @@ function serverPub(server, topRouter) { const urlBookShowAll = "./manifest.json/show/all"; const urlReaderAPI = "/viewer/index_api.html?url=PREFIX" + querystring.escape(urlBook); const urlReaderMaterial = "/viewer/index_material.html?url=PREFIX" + querystring.escape(urlBook); + const urlD2E = "/viewer/index.html?url=PREFIX" + querystring.escape(urlBook); const htmlLanding = "

PATH_STR

" + urlBookShowAll + "

" + - (server.disableReaders ? "" : ("

Reader API:
" + urlReaderAPI + "

Reader Material:
" + urlReaderMaterial + "

")) + + (server.disableReaders ? "" : ("

Reader API:
" + urlReaderAPI + "

Reader Material:
" + urlReaderMaterial + "

D2 Reader:
" + urlD2E + "

")) + ""; const routerPathBase64 = express.Router({ strict: false }); routerPathBase64.use(morgan("combined", { stream: { write: (msg) => debug(msg) } })); diff --git a/package-lock.json b/package-lock.json index 97b98b43..5087dfc3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@d-i-t-a/reader", - "version": "1.4.0", + "version": "1.5.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index c99f9925..e936328a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@d-i-t-a/reader", - "version": "1.4.0", + "version": "1.5.0", "description": "A viewer application for EPUB files.", "repository": "https://github.com/d-i-t-a/R2D2BC", "license": "Apache-2.0", diff --git a/src/index.ts b/src/index.ts index c8b24dde..170dfcc2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,6 +29,8 @@ import { oc } from "ts-optchain" import { TTSSettings } from "./modules/TTS/TTSSettings"; import SearchModule from "./modules/search/SearchModule"; import TextHighlighter from "./modules/highlight/TextHighlighter"; +import { Locator } from "./model/Locator"; +import TimelineModule from "./modules/positions/TimelineModule"; var R2Settings: UserSettings; var R2TTSSettings: TTSSettings; @@ -38,6 +40,7 @@ var BookmarkModuleInstance: BookmarkModule; var AnnotationModuleInstance: AnnotationModule; var TTSModuleInstance: TTSModule; var SearchModuleInstance:SearchModule; +var TimelineModuleInstance: TimelineModule; export const IS_DEV = (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "dev"); @@ -60,6 +63,9 @@ export async function unload() { if (oc(R2Navigator.rights).enableSearch(false)) { SearchModuleInstance.stop() } + if (oc(R2Navigator.rights).enableTimeline(false)) { + TimelineModuleInstance.stop() + } } export function startReadAloud() { if (IS_DEV) { console.log("startReadAloud") } @@ -251,6 +257,18 @@ export async function scroll(value) { R2Settings.scroll(value) } +export async function currentLocator() { + if (IS_DEV) { console.log("currentLocator") } + return R2Navigator.currentLocator() +} +export async function positions() { + if (IS_DEV) { console.log("positions") } + return R2Navigator.positions() +} +export async function goToPosition(value) { + if (IS_DEV) { console.log("goToPosition") } + return R2Navigator.goToPosition(value) +} export async function load(config: ReaderConfig): Promise { var mainElement = document.getElementById("D2Reader-Container"); @@ -275,6 +293,53 @@ export async function load(config: ReaderConfig): Promise { const publication: Publication = await Publication.getManifest(webpubManifestUrl, store); + var startPosition = 0 + var totalContentLength = 0 + var positions = [] + publication.readingOrder.map(async (link, index) => { + var href = publication.getAbsoluteHref(link.href); + await fetch(href) + .then(async r => { + let length = (await r.blob()).size + link.contentLength = length + totalContentLength += length + let positionLength = 1024 + let positionCount = Math.max(1, Math.ceil(length / positionLength)) + if (IS_DEV) console.log(length + " Bytes") + if (IS_DEV) console.log(positionCount + " Positions") + Array.from(Array(positionCount).keys()).map((_, position) => { + const locator: Locator = { + href: link.href, + locations: { + progression: (position) / (positionCount), + position: startPosition + (position + 1), + }, + type: link.type + }; + if (IS_DEV) console.log(locator) + positions.push(locator) + }); + startPosition = startPosition + positionCount + }) + if (index + 1 == publication.readingOrder.length) { + publication.readingOrder.map(async (link) => { + if (IS_DEV) console.log(totalContentLength) + if (IS_DEV) console.log(link.contentLength) + link.contentWeight = 100 / totalContentLength * link.contentLength + if (IS_DEV) console.log(link.contentWeight) + }) + positions.map((locator, _index) => { + let resource = positions.filter((el: Locator) => el.href === locator.href) + let positionIndex = Math.ceil(locator.locations.progression * (resource.length - 1)) + locator.locations.totalProgression = (locator.locations.position - 1) / (positions.length) + locator.locations.remainingPositions = Math.abs((positionIndex) - (resource.length - 1)) + locator.locations.totalRemainingPositions = Math.abs((locator.locations.position - 1) - (positions.length - 1)) + }) + publication.positions = positions + if (IS_DEV) console.log(positions) + } + }); + // Settings R2Settings = await UserSettings.create({ store: settingsStore, @@ -364,6 +429,16 @@ export async function load(config: ReaderConfig): Promise { }); } + // Timeline Module + if (oc(config.rights).enableTimeline(false)) { + TimelineModule.create({ + publication: publication, + delegate: R2Navigator + }).then(function (timelineModule) { + TimelineModuleInstance = timelineModule + }) + } + return new Promise(resolve => resolve(R2Navigator)); } @@ -498,4 +573,13 @@ exports.goToSearchID = function (href, index, current) { } exports.clearSearch = function () { clearSearch() +} +exports.currentLocator = function () { + return currentLocator() +} +exports.positions = function () { + return positions() +} +exports.goToPosition = function (value) { + goToPosition(value) } \ No newline at end of file diff --git a/src/model/Locator.ts b/src/model/Locator.ts index 57814a84..5eb4b0d6 100644 --- a/src/model/Locator.ts +++ b/src/model/Locator.ts @@ -22,9 +22,10 @@ import { IHighlight } from "../modules/highlight/common/highlight"; export interface Locator { href: string; type?: string; - title: string; + title?: string; locations: Locations; text?: LocatorText; + displayInfo?: any; } @@ -39,6 +40,8 @@ export interface Locations { progression?: number; // 3 = bookmarks position?: number; // 4 = goto page totalProgression?: number; + remainingPositions?: number; + totalRemainingPositions?: number; } export interface ReadingPosition extends Locator { diff --git a/src/model/Publication.ts b/src/model/Publication.ts index 55d9e87e..01432c42 100644 --- a/src/model/Publication.ts +++ b/src/model/Publication.ts @@ -18,6 +18,7 @@ */ import Store from "../store/Store"; +import { Locator } from "./Locator"; export interface Metadata { title?: string; @@ -54,6 +55,8 @@ export interface Link { // The MediaOverlays associated to the resource of the Link // mediaOverlays?: MediaOverlays; + contentLength?: number; + contentWeight?: number; } export default class Publication { @@ -65,6 +68,7 @@ export default class Publication { public readonly landmarks: Array; public readonly pageList: Array; public readonly images: Array; + public positions: Array; private readonly manifestUrl: URL; @@ -158,7 +162,11 @@ export default class Publication { } public getRelativeHref(href: string): string | null { const manifest = this.manifestUrl.href.replace("/manifest.json", ""); //new URL(this.manifestUrl.href, this.manifestUrl.href).href; - return href.replace(manifest, ""); + var href = href.replace(manifest, ""); + if(href.charAt(0) === '/') { + href = href.substring(1); + } + return href; } @@ -215,4 +223,12 @@ export default class Publication { } return link } + + /** + * positionsByHref + */ + public positionsByHref(href:string) { + return this.positions.filter((el: Locator) => el.href === href) + } + } diff --git a/src/modules/positions/TimelineModule.ts b/src/modules/positions/TimelineModule.ts new file mode 100644 index 00000000..b42af879 --- /dev/null +++ b/src/modules/positions/TimelineModule.ts @@ -0,0 +1,141 @@ +/* + * Copyright 2018-2020 DITA (AM Consulting LLC) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Developed on behalf of: DITA + * Licensed to: Bibliotheca LLC, Bokbasen AS and CAST under one or more contributor license agreements. + */ + +import { IS_DEV } from "../.."; +import { Locator } from "../../model/Locator"; +import Publication from "../../model/Publication"; +import IFrameNavigator from "../../navigator/IFrameNavigator"; +import { addEventListenerOptional } from "../../utils/EventHandler"; +import ReaderModule from "../ReaderModule"; +import * as HTMLUtilities from "../../utils/HTMLUtilities"; +import { oc } from "ts-optchain"; + +export interface TimelineModuleConfig { + publication: Publication; + delegate: IFrameNavigator; +} + +export default class TimelineModule implements ReaderModule { + + private publication: Publication; + private delegate: IFrameNavigator; + private timelineContainer: HTMLDivElement; + private positionSlider: HTMLInputElement; + + public static async create(config: TimelineModuleConfig) { + const timeline = new this( + config.delegate, + config.publication + ); + + await timeline.start(); + return timeline; + } + + private constructor(delegate: IFrameNavigator, publication: Publication) { + this.delegate = delegate + this.publication = publication + } + + async stop() { + if (IS_DEV) { console.log("Timeline module stop") } + } + + protected async start(): Promise { + + this.delegate.timelineModule = this + + this.timelineContainer = HTMLUtilities.findElement(document, "#container-view-timeline") as HTMLDivElement; + if (oc(this.delegate.rights).enableMaterial(false)) { + this.positionSlider = HTMLUtilities.findElement(document, "#positionSlider") as HTMLInputElement; + } + } + + async initialize() { + return new Promise(async (resolve) => { + await (document as any).fonts.ready; + + let locator = this.delegate.currentLocator() + if (oc(this.delegate.rights).enableMaterial(false)) { + this.positionSlider.value = locator.locations.position.toString() + this.positionSlider.max = (locator.locations.totalRemainingPositions + locator.locations.position).toString() + } + + this.timelineContainer.innerHTML = "" + this.publication.readingOrder.forEach(link => { + + console.log(link.contentWeight) + const linkHref = this.publication.getAbsoluteHref(link.href); + const tocItemAbs = this.publication.getTOCItemAbsolute(linkHref); + const tocHref = (tocItemAbs.href.indexOf("#") !== -1) ? tocItemAbs.href.slice(0, tocItemAbs.href.indexOf("#")) : tocItemAbs.href + const tocHrefAbs = this.publication.getAbsoluteHref(tocHref); + + var chapterHeight + if (this.publication.positions) { + if (link.contentWeight) { + chapterHeight = link.contentWeight + } else { + chapterHeight = 5 + } + } else { + chapterHeight = 100 / this.publication.readingOrder.length + } + + var chapter = document.createElement("div") + chapter.style.height = chapterHeight + "%" + chapter.style.width = "100%" + chapter.className = "chapter"; + + if (tocItemAbs.title !== undefined) { + var tooltip = document.createElement("span") + tooltip.innerHTML = tocItemAbs.title; + tooltip.className = "chapter-tooltip"; + chapter.appendChild(tooltip); + } + + addEventListenerOptional(chapter, 'click', (event: MouseEvent) => { + + event.preventDefault(); + event.stopPropagation(); + + const position1 = this.publication.positions.filter((el: Locator) => el.href === link.href)[0] + position1.href = this.publication.getAbsoluteHref(position1.href) + console.log(position1) + this.delegate.navigate(position1) + }); + + if (tocHrefAbs === this.delegate.currentChapterLink.href) { + chapter.className += " active"; + } else { + chapter.className = chapter.className.replace(" active", ""); + } + + // append bookmarks indicator + // append notes indicator + // append highlights indicator + + this.timelineContainer.appendChild(chapter) + + }); + + resolve() + }); + } + +} diff --git a/src/navigator/IFrameNavigator.ts b/src/navigator/IFrameNavigator.ts index 530c9ffd..d2400c3e 100644 --- a/src/navigator/IFrameNavigator.ts +++ b/src/navigator/IFrameNavigator.ts @@ -30,12 +30,13 @@ import { UserSettingsUIConfig, UserSettings } from "../model/user-settings/UserS import BookmarkModule from "../modules/BookmarkModule"; import AnnotationModule from "../modules/AnnotationModule"; import TTSModule, { TTSSpeechConfig } from "../modules/TTS/TTSModule"; -import { IS_DEV } from ".."; +import { goTo, IS_DEV } from ".."; import Splitting from "../modules/TTS/splitting"; import { oc } from "ts-optchain"; import ReflowableBookView from "../views/ReflowableBookView"; import SearchModule from "../modules/search/SearchModule"; import TextHighlighter from "../modules/highlight/TextHighlighter"; +import TimelineModule from "../modules/positions/TimelineModule"; export interface UpLinkConfig { url?: URL; @@ -85,6 +86,7 @@ export interface ReaderRights { enableTTS?: boolean; enableSearch?: boolean; enableMaterial?: boolean; + enableTimeline?: boolean; } export interface ReaderUI { @@ -121,6 +123,7 @@ export default class IFrameNavigator implements Navigator { ttsModule?: TTSModule; searchModule?: SearchModule; highlighter?: TextHighlighter; + timelineModule?: TimelineModule; sideNavExpanded: boolean = false material: boolean = false @@ -175,6 +178,7 @@ export default class IFrameNavigator implements Navigator { private bookTitle: HTMLSpanElement; private chapterTitle: HTMLSpanElement; private chapterPosition: HTMLSpanElement; + private remainingPositions: HTMLSpanElement; private newPosition: Locator | null; private newElementId: string | null; private isBeingStyled: boolean; @@ -290,14 +294,16 @@ export default class IFrameNavigator implements Navigator { // Main Element this.iframe = HTMLUtilities.findRequiredElement(mainElement, "main#iframe-wrapper iframe") as HTMLIFrameElement; - - this.loadingMessage = HTMLUtilities.findRequiredElement(mainElement, "#reader-loading") as HTMLDivElement; - this.loadingMessage.innerHTML = readerLoading; - this.loadingMessage.style.display = "none"; - - this.errorMessage = HTMLUtilities.findRequiredElement(mainElement, "#reader-error") as HTMLDivElement; - this.errorMessage.innerHTML = readerError; - this.errorMessage.style.display = "none"; + this.loadingMessage = HTMLUtilities.findElement(mainElement, "#reader-loading") as HTMLDivElement; + if (this.loadingMessage) { + this.loadingMessage.innerHTML = readerLoading; + this.loadingMessage.style.display = "none"; + } + this.errorMessage = HTMLUtilities.findElement(mainElement, "#reader-error") as HTMLDivElement; + if (this.errorMessage) { + this.errorMessage.innerHTML = readerError; + this.errorMessage.style.display = "none"; + } this.tryAgainButton = HTMLUtilities.findElement(mainElement, "button[class=try-again]") as HTMLButtonElement; this.goBackButton = HTMLUtilities.findElement(mainElement, "button[class=go-back]") as HTMLButtonElement; @@ -306,8 +312,9 @@ export default class IFrameNavigator implements Navigator { if (this.headerMenu) this.bookTitle = HTMLUtilities.findElement(this.headerMenu, "#book-title") as HTMLSpanElement; - if (this.infoBottom) this.chapterTitle = HTMLUtilities.findRequiredElement(this.infoBottom, "span[class=chapter-title]") as HTMLSpanElement; - if (this.infoBottom) this.chapterPosition = HTMLUtilities.findRequiredElement(this.infoBottom, "span[class=chapter-position]") as HTMLSpanElement; + if (this.infoBottom) this.chapterTitle = HTMLUtilities.findElement(this.infoBottom, "span[class=chapter-title]") as HTMLSpanElement; + if (this.infoBottom) this.chapterPosition = HTMLUtilities.findElement(this.infoBottom, "span[class=chapter-position]") as HTMLSpanElement; + if (this.infoBottom) this.remainingPositions = HTMLUtilities.findElement(this.infoBottom, "span[class=remaining-positions]") as HTMLSpanElement; if (this.headerMenu) this.espandMenuIcon = HTMLUtilities.findElement(this.headerMenu, "#expand-menu") as HTMLElement; @@ -423,8 +430,10 @@ export default class IFrameNavigator implements Navigator { self.annotationModule.drawHighlights() // self.annotationModule.drawIndicators() } else { - await this.highlighter.destroyAllhighlights(this.iframe.contentDocument) - self.searchModule.drawSearch() + if (oc(this.rights).enableSearch(false)) { + await this.highlighter.destroyAllhighlights(this.iframe.contentDocument) + self.searchModule.drawSearch() + } } }, 300); @@ -559,6 +568,7 @@ export default class IFrameNavigator implements Navigator { if (this.previousPageAnchorElement) this.previousPageAnchorElement.style.display = "unset" if (this.chapterTitle) this.chapterTitle.style.display = "inline"; if (this.chapterPosition) this.chapterPosition.style.display = "inline"; + if (this.remainingPositions) this.remainingPositions.style.display = "inline"; if (this.eventHandler) { this.eventHandler.onInternalLink = this.handleInternalLink.bind(this); this.eventHandler.onClickThrough = this.handleClickThrough.bind(this); @@ -626,6 +636,7 @@ export default class IFrameNavigator implements Navigator { if (this.chapterTitle) this.chapterTitle.style.display = "none"; if (this.chapterPosition) this.chapterPosition.style.display = "none"; + if (this.remainingPositions) this.remainingPositions.style.display = "none"; if (this.eventHandler) { this.eventHandler.onInternalLink = this.handleInternalLink.bind(this); this.eventHandler.onClickThrough = this.handleClickThrough.bind(this); @@ -644,10 +655,12 @@ export default class IFrameNavigator implements Navigator { if (this.annotationModule !== undefined) { this.annotationModule.drawHighlights() } else { - await this.highlighter.destroyAllhighlights(this.iframe.contentDocument) - this.searchModule.drawSearch() + if (oc(this.rights).enableSearch(false)) { + await this.highlighter.destroyAllhighlights(this.iframe.contentDocument) + this.searchModule.drawSearch() + } } - }, 100); + }, 200); } @@ -872,7 +885,7 @@ export default class IFrameNavigator implements Navigator { } private async handleIFrameLoad(): Promise { - this.errorMessage.style.display = "none"; + if (this.errorMessage) this.errorMessage.style.display = "none"; this.showLoadingMessageAfterDelay(); try { let bookViewPosition = 0; @@ -887,7 +900,6 @@ export default class IFrameNavigator implements Navigator { setTimeout(() => { this.reflowable.goToPosition(bookViewPosition); - this.updatePositionInfo(); }, 100); setTimeout(() => { @@ -900,7 +912,9 @@ export default class IFrameNavigator implements Navigator { let currentLocation = this.currentChapterLink.href - this.updatePositionInfo(); + setTimeout(() => { + this.updatePositionInfo(); + }, 200); const previous = this.publication.getPreviousSpineItem(currentLocation); if (previous && previous.href) { @@ -1050,6 +1064,13 @@ export default class IFrameNavigator implements Navigator { }, 100); + setTimeout(async () => { + + if (this.timelineModule !== undefined) { + await this.timelineModule.initialize() + } + + }, 100); return new Promise(resolve => resolve()); @@ -1061,7 +1082,7 @@ export default class IFrameNavigator implements Navigator { } private abortOnError() { - this.errorMessage.style.display = "block"; + if (this.errorMessage) this.errorMessage.style.display = "block"; if (this.isLoading) { this.hideLoadingMessage(); } @@ -1150,20 +1171,13 @@ export default class IFrameNavigator implements Navigator { private hideModal(modal: HTMLDivElement, control?: HTMLAnchorElement | HTMLButtonElement) { // Restore the page for screen readers. this.iframe.setAttribute("aria-hidden", "false"); - if (this.upLink) { - this.upLink.setAttribute("aria-hidden", "false"); - } - if (this.linksBottom) { - this.linksBottom.setAttribute("aria-hidden", "false"); - } - if (this.linksMiddle) { - this.linksMiddle.setAttribute("aria-hidden", "false"); - } - this.loadingMessage.setAttribute("aria-hidden", "false"); - this.errorMessage.setAttribute("aria-hidden", "false"); - this.infoTop.setAttribute("aria-hidden", "false"); - this.infoBottom.setAttribute("aria-hidden", "false"); - + if (this.upLink) this.upLink.setAttribute("aria-hidden", "false"); + if (this.linksBottom) this.linksBottom.setAttribute("aria-hidden", "false"); + if (this.linksMiddle) this.linksMiddle.setAttribute("aria-hidden", "false"); + if (this.loadingMessage) this.loadingMessage.setAttribute("aria-hidden", "false"); + if (this.errorMessage) this.errorMessage.setAttribute("aria-hidden", "false"); + if (this.infoTop) this.infoTop.setAttribute("aria-hidden", "false"); + if (this.infoBottom) this.infoBottom.setAttribute("aria-hidden", "false"); this.hideElement(modal, control); } @@ -1274,7 +1288,25 @@ export default class IFrameNavigator implements Navigator { this.stopReadAloud(); this.navigate(position); } + currentLocator():Locator { + let positions = this.publication.positionsByHref(this.publication.getRelativeHref(this.currentTOCRawLink)); + let positionIndex = Math.ceil(this.reflowable.getCurrentPosition() * (positions.length - 1)) + let position = positions[positionIndex] + position.locations.progression = this.reflowable.getCurrentPosition() + position.displayInfo = { + resourceScreenIndex : Math.round(this.reflowable.getCurrentPage()), + resourceScreenCount : Math.round(this.reflowable.getPageCount()) + } + return position + } + positions():any { + return this.publication.positions + } + goToPosition(position:number) { + let locator = this.publication.positions.filter((el: Locator) => el.locations.position == position)[0] + goTo(locator) + } private handlePreviousPageClick(event: MouseEvent | TouchEvent | KeyboardEvent): void { this.stopReadAloud(); if(this.reflowable.isPaginated()) { @@ -1479,18 +1511,24 @@ export default class IFrameNavigator implements Navigator { if (this.annotationModule !== undefined) { this.annotationModule.handleResize() } else { - this.searchModule.handleResize() + if (oc(this.rights).enableSearch(false)) { + this.searchModule.handleResize() + } } }, 100); } updatePositionInfo() { if(this.reflowable.isPaginated()) { - const currentPage = Math.round(this.reflowable.getCurrentPage()); - const pageCount = Math.round(this.reflowable.getPageCount()); + const locator = this.currentLocator() + const currentPage = locator.displayInfo.resourceScreenIndex + const pageCount = locator.displayInfo.resourceScreenCount + const remaining = locator.locations.remainingPositions; if (this.chapterPosition) this.chapterPosition.innerHTML = "Page " + currentPage + " of " + pageCount; + if (this.remainingPositions) this.remainingPositions.innerHTML = remaining + " left in chapter"; } else { if (this.chapterPosition) this.chapterPosition.innerHTML = ""; + if (this.remainingPositions) this.remainingPositions.innerHTML = ""; } } @@ -1603,8 +1641,10 @@ export default class IFrameNavigator implements Navigator { this.annotationModule.drawHighlights() this.annotationModule.showHighlights(); } else { - await this.highlighter.destroyAllhighlights(this.iframe.contentDocument) - this.searchModule.drawSearch() + if (oc(this.rights).enableSearch(false)) { + await this.highlighter.destroyAllhighlights(this.iframe.contentDocument) + this.searchModule.drawSearch() + } } if(this.reflowable.isScrollmode()) { @@ -1657,7 +1697,7 @@ export default class IFrameNavigator implements Navigator { private showLoadingMessageAfterDelay() { this.isLoading = true; setTimeout(() => { - if (this.isLoading) { + if (this.isLoading && this.loadingMessage) { this.loadingMessage.style.display = "block"; this.loadingMessage.classList.add("is-loading"); } @@ -1671,8 +1711,10 @@ export default class IFrameNavigator implements Navigator { private hideLoadingMessage() { this.isLoading = false; - this.loadingMessage.style.display = "none"; - this.loadingMessage.classList.remove("is-loading"); + if (this.loadingMessage) { + this.loadingMessage.style.display = "none"; + this.loadingMessage.classList.remove("is-loading"); + } } private async saveCurrentReadingPosition(): Promise { diff --git a/src/styles/sass/reader.scss b/src/styles/sass/reader.scss index 641f2110..a6e621eb 100644 --- a/src/styles/sass/reader.scss +++ b/src/styles/sass/reader.scss @@ -43,3 +43,5 @@ @import "reader/toolbox"; // TTS ui tools @import "reader/tts"; + +@import "reader/timeline"; diff --git a/src/styles/sass/reader/_global.scss b/src/styles/sass/reader/_global.scss index 532523ea..95f43e52 100644 --- a/src/styles/sass/reader/_global.scss +++ b/src/styles/sass/reader/_global.scss @@ -60,6 +60,11 @@ box-sizing: border-box; } + #reader-info-bottom { + bottom: 0px; + position: fixed; + width: 100%; + } .info { color: $ui-dark-gray; margin: 0; @@ -87,7 +92,8 @@ } .chapter-position, - .chapter-title { + .chapter-title, + .remaining-positions { font-size: 0.85rem; font-variant-numeric: lining-nums tabular-nums; } diff --git a/src/styles/sass/reader/_timeline.scss b/src/styles/sass/reader/_timeline.scss new file mode 100644 index 00000000..a0a8927c --- /dev/null +++ b/src/styles/sass/reader/_timeline.scss @@ -0,0 +1,59 @@ +/* + * Copyright 2018-2020 DITA (AM Consulting LLC) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Developed on behalf of: DITA + * Licensed to: Bibliotheca LLC, Bokbasen AS and CAST under one or more contributor license agreements. + */ + + .timeline { + position: fixed; + top: 1.5rem; + left: 2.5rem; + bottom: 1.5rem; + width: 1.25rem; + .chapter { + border-left: 6px solid transparent; + transition: border-color 300ms ease-out; + background: $ui-light-gray; + border: 1px solid $ui-white; + border-radius: 5px; + position: relative; + &:hover { + background-color: $ui-light-gray; + .chapter-tooltip { + display: block; + } + } + &.active { + background-color: $ui-default; + } + } + .chapter-tooltip { + display: none; + position: absolute; + left: 2rem; + top: 50%; + transform: translate(0,-50%); + background: $ui-light-gray; + padding: 0.25rem 0.5rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 22rem; + } +} + + + diff --git a/viewer/index.html b/viewer/index.html new file mode 100644 index 00000000..6b0a25db --- /dev/null +++ b/viewer/index.html @@ -0,0 +1,664 @@ + + + + + + + D2 Reader + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + +
+ +
+
+
+
+
+ +
+ +
+
+ + +
+
+ +
+
+ +
+
+
+
+
+ + + + +
+
+ +
+ +
+ + +
+ +
+
+ + + + + +
+
+ +
+
+ +
+ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/viewer/index_api.html b/viewer/index_api.html index 8add6df7..5d2320f6 100644 --- a/viewer/index_api.html +++ b/viewer/index_api.html @@ -25,7 +25,8 @@ - + @@ -42,7 +43,7 @@
-
+

- -
-
-
-
-
-
+ +
+
+
+
+
+


@@ -69,7 +76,16 @@


-
+
+
+


@@ -84,7 +100,8 @@

-


@@ -93,7 +110,8 @@


-
+



@@ -112,8 +130,12 @@


-
-
+
+

@@ -123,9 +145,11 @@


-
-


@@ -222,25 +246,56 @@ +
+
- - - - + + + +
- +
@@ -308,8 +363,8 @@ } var selectionMenuItems = [ - { - id: 'anIcon', + { + id: 'anIcon', callback: function (selection) { alert(selection) } @@ -326,20 +381,21 @@ enableBookmarks: true, enableAnnotations: true, enableTTS: true, - enableSearch: true + enableSearch: true, + enableTimeline: true }, tts: { enableSplitter: true, - // highlight: "lines", - // autoScroll: false, - // rate: 1.2, - // pitch: 1.0, - // volume: 0.5, - // voice: { - // usePublication: true, - // name : "Daniel", - // lang : "en-GB", - // } + // highlight: "lines", + // autoScroll: false, + // rate: 1.2, + // pitch: 1.0, + // volume: 0.5, + // voice: { + // usePublication: true, + // name : "Daniel", + // lang : "en-GB", + // } }, search: { color: "#fce300", diff --git a/viewer/index_material.html b/viewer/index_material.html index 68fc78ac..8f4588a6 100644 --- a/viewer/index_material.html +++ b/viewer/index_material.html @@ -25,7 +25,8 @@ - + @@ -50,34 +51,41 @@
    -
  • D2 Reader +
  • D2 Reader import_contacts unfold_more
  • @@ -139,24 +147,24 @@
    • -
    • @@ -167,16 +175,16 @@
      • @@ -211,30 +219,30 @@
        • -
        • -
        • @@ -260,11 +268,13 @@
- - @@ -273,22 +283,46 @@
- +
- - - + + +
- +