diff --git a/CHANGELOG.md b/CHANGELOG.md index 959d97d76..97f3c743c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,8 +13,8 @@ All notable changes to this project will be documented in this file. Take a look * This callback is called before executing any navigation action. * Useful for hiding UI elements when the user navigates, or implementing analytics. * Added swipe gesture support for navigating in PDF paginated spread mode. -* Added `fit` preference for PDF documents to control how pages are scaled within the viewport. - * Only effective in scroll mode. Paginated mode always uses page fit due to PDFKit limitations. +* Added `fit` preference for fixed-layout publications (PDF and FXL EPUB) to control how pages are scaled within the viewport. + * In the PDF navigator, it is only effective in scroll mode. Paginated mode always uses `page` fit due to PDFKit limitations. ### Deprecated @@ -32,7 +32,7 @@ All notable changes to this project will be documented in this file. Take a look * The `Fit` enum has been redesigned to fit the PDF implementation. * **Breaking change:** Update any code using the old `Fit` enum values. -* The PDF navigator's content inset behavior has changed: +* The fixed-layout navigators (PDF and FXL EPUB)'s content inset behavior has changed: * iPhone: Continues to apply window safe area insets (to account for notch/Dynamic Island). * iPad/macOS: Now displays edge-to-edge with no automatic safe area insets. * You can customize this behavior with `VisualNavigatorDelegate.navigatorContentInset(_:)`. diff --git a/Sources/Navigator/EPUB/Assets/Static/scripts/readium-fixed-wrapper-one.js b/Sources/Navigator/EPUB/Assets/Static/scripts/readium-fixed-wrapper-one.js index a1f95ec21..fbfbd2519 100644 --- a/Sources/Navigator/EPUB/Assets/Static/scripts/readium-fixed-wrapper-one.js +++ b/Sources/Navigator/EPUB/Assets/Static/scripts/readium-fixed-wrapper-one.js @@ -1,2 +1,2 @@ -(()=>{"use strict";var t={};t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"==typeof window)return window}}();var e=function(t){var e=null,n=null,i=null,o=document.getElementById("page");o.addEventListener("load",(function(){var t=o.contentWindow.document.querySelector("meta[name=viewport]");if(t){for(var n,i=/(\w+) *= *([^\s,]+)/g,l={};n=i.exec(t.content);)l[n[1]]=n[2];var a=Number.parseFloat(l.width),s=Number.parseFloat(l.height);a&&s&&(e={width:a,height:s},r())}}));var l=o.closest(".viewport");function r(){if(e&&n&&i){o.style.width=e.width+"px",o.style.height=e.height+"px",o.style.marginTop=i.top-i.bottom+"px";var t=n.width/e.width,l=n.height/e.height,r=Math.min(t,l);document.querySelector("meta[name=viewport]").content="initial-scale="+r+", minimum-scale="+r}}return{isLoading:!1,link:null,load:function(t,e){if(t.link&&t.url){var n=this;n.link=t.link,n.isLoading=!0,o.addEventListener("load",(function i(){o.removeEventListener("load",i),setTimeout((function(){n.isLoading=!1,o.contentWindow.eval(`readium.link = ${JSON.stringify(t.link)};`),e&&e()}),100)})),o.src=t.url}else e&&e()},reset:function(){this.link&&(this.link=null,e=null,o.src="about:blank")},eval:function(t){if(this.link&&!this.isLoading)return o.contentWindow.eval(t)},setViewport:function(t,e){n=t,i=e,r()},show:function(){l.style.display="block"},hide:function(){l.style.display="none"}}}();t.g.spread={load:function(t){0!==t.length&&e.load(t[0],(function(){webkit.messageHandlers.spreadLoaded.postMessage({})}))},eval:function(t,n){var i;if("#"===t||""===t||(null===(i=e.link)||void 0===i?void 0:i.href)===t)return e.eval(n)},setViewport:function(t,n){e.setViewport(t,n)}}})(); +(()=>{"use strict";var t={};t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"==typeof window)return window}}();const e={SINGLE:"single",SPREAD_LEFT:"spread-left",SPREAD_RIGHT:"spread-right",SPREAD_CENTER:"spread-center"},n={AUTO:"auto",PAGE:"page",WIDTH:"width"};var i=function(t,i){var o=null,l=null,a=null,r=n.AUTO,s=Object.values(e).includes(i)?i:e.SINGLE,u=document.getElementById("page");u.addEventListener("load",(function(){var t=u.contentWindow.document.querySelector("meta[name=viewport]");if(t){for(var e,n=/(\w+) *= *([^\s,]+)/g,i={};e=n.exec(t.content);)i[e[1]]=e[2];var l=Number.parseFloat(i.width),a=Number.parseFloat(i.height);l&&a&&(o={width:l,height:a},d())}}));var c=u.closest(".viewport");function d(){if(o&&l&&a){u.style.width=o.width+"px",u.style.height=o.height+"px";var t,i=l.width/o.width,c=l.height/o.height;t=r===n.WIDTH?i:Math.min(i,c);var d=o.height*t,h=s===e.SINGLE||s===e.SPREAD_CENTER;if(r===n.WIDTH&&d>l.height)u.style.top=a.top+"px",u.style.transform=h?"translateX(-50%)":"none";else{var f=a.top-a.bottom;u.style.top="calc(50% + "+f+"px)",u.style.transform=h?"translate(-50%, -50%)":"translateY(-50%)"}document.querySelector("meta[name=viewport]").content="initial-scale="+t+", minimum-scale="+t}}return{isLoading:!1,link:null,load:function(t,e){if(t.link&&t.url){var n=this;n.link=t.link,n.isLoading=!0,u.addEventListener("load",(function i(){u.removeEventListener("load",i),setTimeout((function(){n.isLoading=!1,u.contentWindow.eval(`readium.link = ${JSON.stringify(t.link)};`),e&&e()}),100)})),u.src=t.url}else e&&e()},reset:function(){this.link&&(this.link=null,o=null,u.src="about:blank")},eval:function(t){if(this.link&&!this.isLoading)return u.contentWindow.eval(t)},setViewport:function(t,e,i){l=t,a=e,Object.values(n).includes(i)&&(r=i),d()},show:function(){c.style.display="block"},hide:function(){c.style.display="none"}}}(0,e.SINGLE);t.g.spread={load:function(t){0!==t.length&&i.load(t[0],(function(){webkit.messageHandlers.spreadLoaded.postMessage({})}))},eval:function(t,e){var n;if("#"===t||""===t||(null===(n=i.link)||void 0===n?void 0:n.href)===t)return i.eval(e)},setViewport:function(t,e,n){i.setViewport(t,e,n)}}})(); //# sourceMappingURL=readium-fixed-wrapper-one.js.map \ No newline at end of file diff --git a/Sources/Navigator/EPUB/Assets/Static/scripts/readium-fixed-wrapper-two.js b/Sources/Navigator/EPUB/Assets/Static/scripts/readium-fixed-wrapper-two.js index 8ee445d82..db8191ee7 100644 --- a/Sources/Navigator/EPUB/Assets/Static/scripts/readium-fixed-wrapper-two.js +++ b/Sources/Navigator/EPUB/Assets/Static/scripts/readium-fixed-wrapper-two.js @@ -1,2 +1,2 @@ -(()=>{"use strict";var t={};function e(t){var e=null,n=null,i=null,o=document.getElementById(t);o.addEventListener("load",(function(){var t=o.contentWindow.document.querySelector("meta[name=viewport]");if(t){for(var n,i=/(\w+) *= *([^\s,]+)/g,r={};n=i.exec(t.content);)r[n[1]]=n[2];var a=Number.parseFloat(r.width),s=Number.parseFloat(r.height);a&&s&&(e={width:a,height:s},l())}}));var r=o.closest(".viewport");function l(){if(e&&n&&i){o.style.width=e.width+"px",o.style.height=e.height+"px",o.style.marginTop=i.top-i.bottom+"px";var t=n.width/e.width,r=n.height/e.height,l=Math.min(t,r);document.querySelector("meta[name=viewport]").content="initial-scale="+l+", minimum-scale="+l}}return{isLoading:!1,link:null,load:function(t,e){if(t.link&&t.url){var n=this;n.link=t.link,n.isLoading=!0,o.addEventListener("load",(function i(){o.removeEventListener("load",i),setTimeout((function(){n.isLoading=!1,o.contentWindow.eval(`readium.link = ${JSON.stringify(t.link)};`),e&&e()}),100)})),o.src=t.url}else e&&e()},reset:function(){this.link&&(this.link=null,e=null,o.src="about:blank")},eval:function(t){if(this.link&&!this.isLoading)return o.contentWindow.eval(t)},setViewport:function(t,e){n=t,i=e,l()},show:function(){r.style.display="block"},hide:function(){r.style.display="none"}}}t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"==typeof window)return window}}();var n={left:e("page-left"),right:e("page-right"),center:e("page-center")};function i(t){for(const e in n)t(n[e])}t.g.spread={load:function(t){function e(){n.left.isLoading||n.right.isLoading||n.center.isLoading||webkit.messageHandlers.spreadLoaded.postMessage({})}i((function(t){t.reset(),t.hide()}));for(const i in t){const o=t[i],r=n[o.page];r&&(r.show(),r.load(o,e))}},eval:function(t,e){if("#"===t||""===t)i((function(t){t.eval(e)}));else{var o=function(t){for(const o in n){var e,i=n[o];if((null===(e=i.link)||void 0===e?void 0:e.href)===t)return i}return null}(t);if(o)return o.eval(e)}},setViewport:function(t,e){t.width/=2,n.left.setViewport(t,{top:e.top,right:0,bottom:e.bottom,left:e.left}),n.right.setViewport(t,{top:e.top,right:e.right,bottom:e.bottom,left:0}),n.center.setViewport(t,{top:e.top,right:0,bottom:e.bottom,left:0})}}})(); +(()=>{"use strict";var t={};t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"==typeof window)return window}}();const e={SINGLE:"single",SPREAD_LEFT:"spread-left",SPREAD_RIGHT:"spread-right",SPREAD_CENTER:"spread-center"},n={AUTO:"auto",PAGE:"page",WIDTH:"width"};function i(t,i){var o=null,r=null,l=null,a=n.AUTO,s=Object.values(e).includes(i)?i:e.SINGLE,c=document.getElementById(t);c.addEventListener("load",(function(){var t=c.contentWindow.document.querySelector("meta[name=viewport]");if(t){for(var e,n=/(\w+) *= *([^\s,]+)/g,i={};e=n.exec(t.content);)i[e[1]]=e[2];var r=Number.parseFloat(i.width),l=Number.parseFloat(i.height);r&&l&&(o={width:r,height:l},d())}}));var u=c.closest(".viewport");function d(){if(o&&r&&l){c.style.width=o.width+"px",c.style.height=o.height+"px";var t,i=r.width/o.width,u=r.height/o.height;t=a===n.WIDTH?i:Math.min(i,u);var d=o.height*t,f=s===e.SINGLE||s===e.SPREAD_CENTER;if(a===n.WIDTH&&d>r.height)c.style.top=l.top+"px",c.style.transform=f?"translateX(-50%)":"none";else{var h=l.top-l.bottom;c.style.top="calc(50% + "+h+"px)",c.style.transform=f?"translate(-50%, -50%)":"translateY(-50%)"}document.querySelector("meta[name=viewport]").content="initial-scale="+t+", minimum-scale="+t}}return{isLoading:!1,link:null,load:function(t,e){if(t.link&&t.url){var n=this;n.link=t.link,n.isLoading=!0,c.addEventListener("load",(function i(){c.removeEventListener("load",i),setTimeout((function(){n.isLoading=!1,c.contentWindow.eval(`readium.link = ${JSON.stringify(t.link)};`),e&&e()}),100)})),c.src=t.url}else e&&e()},reset:function(){this.link&&(this.link=null,o=null,c.src="about:blank")},eval:function(t){if(this.link&&!this.isLoading)return c.contentWindow.eval(t)},setViewport:function(t,e,i){r=t,l=e,Object.values(n).includes(i)&&(a=i),d()},show:function(){u.style.display="block"},hide:function(){u.style.display="none"}}}var o={left:i("page-left",e.SPREAD_LEFT),right:i("page-right",e.SPREAD_RIGHT),center:i("page-center",e.SPREAD_CENTER)};function r(t){for(const e in o)t(o[e])}t.g.spread={load:function(t){function e(){o.left.isLoading||o.right.isLoading||o.center.isLoading||webkit.messageHandlers.spreadLoaded.postMessage({})}r((function(t){t.reset(),t.hide()}));for(const n in t){const i=t[n],r=o[i.page];r&&(r.show(),r.load(i,e))}},eval:function(t,e){if("#"===t||""===t)r((function(t){t.eval(e)}));else{var n=function(t){for(const i in o){var e,n=o[i];if((null===(e=n.link)||void 0===e?void 0:e.href)===t)return n}return null}(t);if(n)return n.eval(e)}},setViewport:function(t,e,n){t.width/=2,o.left.setViewport(t,{top:e.top,right:0,bottom:e.bottom,left:e.left},n),o.right.setViewport(t,{top:e.top,right:e.right,bottom:e.bottom,left:0},n),o.center.setViewport(t,{top:e.top,right:0,bottom:e.bottom,left:0},n)}}})(); //# sourceMappingURL=readium-fixed-wrapper-two.js.map \ No newline at end of file diff --git a/Sources/Navigator/EPUB/EPUBFixedSpreadView.swift b/Sources/Navigator/EPUB/EPUBFixedSpreadView.swift index 6b529f4b3..a3a5cc5e7 100644 --- a/Sources/Navigator/EPUB/EPUBFixedSpreadView.swift +++ b/Sources/Navigator/EPUB/EPUBFixedSpreadView.swift @@ -88,11 +88,13 @@ final class EPUBFixedSpreadView: EPUBSpreadView { insets.right = horizontalInsets let viewportSize = bounds.inset(by: insets).size + let fitString = viewModel.settings.fit.rawValue webView.evaluateJavaScript(""" spread.setViewport( {'width': \(Int(viewportSize.width)), 'height': \(Int(viewportSize.height))}, - {'top': \(Int(insets.top)), 'left': \(Int(insets.left)), 'bottom': \(Int(insets.bottom)), 'right': \(Int(insets.right))} + {'top': \(Int(insets.top)), 'left': \(Int(insets.left)), 'bottom': \(Int(insets.bottom)), 'right': \(Int(insets.right))}, + '\(fitString)' ); """) } diff --git a/Sources/Navigator/EPUB/EPUBNavigatorViewController.swift b/Sources/Navigator/EPUB/EPUBNavigatorViewController.swift index aab8d1393..6fa0056dd 100644 --- a/Sources/Navigator/EPUB/EPUBNavigatorViewController.swift +++ b/Sources/Navigator/EPUB/EPUBNavigatorViewController.swift @@ -1043,10 +1043,23 @@ extension EPUBNavigatorViewController: EPUBSpreadViewDelegate { // the application's bars. var insets = view.window?.safeAreaInsets ?? .zero - if publication.metadata.layout != .fixed { + switch publication.metadata.layout ?? .reflowable { + case .fixed: + // With iPadOS and macOS, we aim to display content edge-to-edge + // since there are no physical notches or Dynamic Island like on the + // iPhone. + if UIDevice.current.userInterfaceIdiom != .phone { + insets = .zero + } + + case .reflowable: let configInset = config.contentInset(for: view.traitCollection.verticalSizeClass) insets.top = max(insets.top, configInset.top) insets.bottom = max(insets.bottom, configInset.bottom) + + case .scrolled: + // Not supported with EPUB. + break } return insets diff --git a/Sources/Navigator/EPUB/EPUBNavigatorViewModel.swift b/Sources/Navigator/EPUB/EPUBNavigatorViewModel.swift index 6e93fecf8..67b89d0f2 100644 --- a/Sources/Navigator/EPUB/EPUBNavigatorViewModel.swift +++ b/Sources/Navigator/EPUB/EPUBNavigatorViewModel.swift @@ -220,6 +220,7 @@ final class EPUBNavigatorViewModel: Loggable { || oldSettings.verticalText != newSettings.verticalText || oldSettings.scroll != newSettings.scroll || oldSettings.spread != newSettings.spread + || oldSettings.fit != newSettings.fit // We don't commit the CSS changes if we invalidate the pagination, as // the resources will be reloaded anyway. diff --git a/Sources/Navigator/EPUB/Preferences/EPUBPreferences.swift b/Sources/Navigator/EPUB/Preferences/EPUBPreferences.swift index ec9289a09..8eb31beb8 100644 --- a/Sources/Navigator/EPUB/Preferences/EPUBPreferences.swift +++ b/Sources/Navigator/EPUB/Preferences/EPUBPreferences.swift @@ -18,6 +18,13 @@ public struct EPUBPreferences: ConfigurablePreferences { /// spread). public var columnCount: ColumnCount? + /// Method for fitting the content of a fixed-layout publication within the + /// viewport. + /// + /// - `auto` or `page`: Fit entire page within viewport (default). + /// - `width`: Fit page width, allow vertical scrolling if needed. + public var fit: Fit? + /// Default typeface for the text. public var fontFamily: FontFamily? @@ -97,6 +104,7 @@ public struct EPUBPreferences: ConfigurablePreferences { public init( backgroundColor: Color? = nil, columnCount: ColumnCount? = nil, + fit: Fit? = nil, fontFamily: FontFamily? = nil, fontSize: Double? = nil, fontWeight: Double? = nil, @@ -123,6 +131,7 @@ public struct EPUBPreferences: ConfigurablePreferences { ) { self.backgroundColor = backgroundColor self.columnCount = columnCount + self.fit = fit self.fontFamily = fontFamily self.fontSize = fontSize.map { max($0, 0) } self.fontWeight = fontWeight?.clamped(to: 0.0 ... 2.5) @@ -152,6 +161,7 @@ public struct EPUBPreferences: ConfigurablePreferences { EPUBPreferences( backgroundColor: other.backgroundColor ?? backgroundColor, columnCount: other.columnCount ?? columnCount, + fit: other.fit ?? fit, fontFamily: other.fontFamily ?? fontFamily, fontSize: other.fontSize ?? fontSize, fontWeight: other.fontWeight ?? fontWeight, diff --git a/Sources/Navigator/EPUB/Preferences/EPUBPreferencesEditor.swift b/Sources/Navigator/EPUB/Preferences/EPUBPreferencesEditor.swift index 98843dc8f..4cfab0009 100644 --- a/Sources/Navigator/EPUB/Preferences/EPUBPreferencesEditor.swift +++ b/Sources/Navigator/EPUB/Preferences/EPUBPreferencesEditor.swift @@ -70,6 +70,18 @@ public final class EPUBPreferencesEditor: StatefulPreferencesEditor = + enumPreference( + preference: \.fit, + setting: \.fit, + defaultEffectiveValue: defaults.fit ?? .auto, + isEffective: { [layout] _ in layout == .fixed }, + supportedValues: [.auto, .page, .width] + ) + /// Default typeface for the text. /// /// Only effective with reflowable publications. diff --git a/Sources/Navigator/EPUB/Preferences/EPUBSettings.swift b/Sources/Navigator/EPUB/Preferences/EPUBSettings.swift index 7e545edbf..f1a7f48b9 100644 --- a/Sources/Navigator/EPUB/Preferences/EPUBSettings.swift +++ b/Sources/Navigator/EPUB/Preferences/EPUBSettings.swift @@ -13,6 +13,7 @@ import ReadiumShared public struct EPUBSettings: ConfigurableSettings { public var backgroundColor: Color? public var columnCount: ColumnCount + public var fit: Fit public var fontFamily: FontFamily? public var fontSize: Double public var fontWeight: Double? @@ -46,6 +47,7 @@ public struct EPUBSettings: ConfigurableSettings { public init( backgroundColor: Color?, columnCount: ColumnCount, + fit: Fit, fontFamily: FontFamily?, fontSize: Double, fontWeight: Double?, @@ -72,6 +74,7 @@ public struct EPUBSettings: ConfigurableSettings { ) { self.backgroundColor = backgroundColor self.columnCount = columnCount + self.fit = fit self.fontFamily = fontFamily self.fontSize = fontSize self.fontWeight = fontWeight @@ -139,6 +142,9 @@ public struct EPUBSettings: ConfigurableSettings { columnCount: preferences.columnCount ?? defaults.columnCount ?? .auto, + fit: preferences.fit + ?? defaults.fit + ?? .auto, fontFamily: preferences.fontFamily, fontSize: preferences.fontSize ?? defaults.fontSize @@ -196,6 +202,7 @@ public struct EPUBSettings: ConfigurableSettings { /// See `EPUBPreferences`. public struct EPUBDefaults { public var columnCount: ColumnCount? + public var fit: Fit? public var fontSize: Double? public var fontWeight: Double? public var hyphens: Bool? @@ -218,6 +225,7 @@ public struct EPUBDefaults { public init( columnCount: ColumnCount? = nil, + fit: Fit? = nil, fontSize: Double? = nil, fontWeight: Double? = nil, hyphens: Bool? = nil, @@ -239,6 +247,7 @@ public struct EPUBDefaults { wordSpacing: Double? = nil ) { self.columnCount = columnCount + self.fit = fit self.fontSize = fontSize self.fontWeight = fontWeight self.hyphens = hyphens diff --git a/Sources/Navigator/EPUB/Scripts/src/fixed-page.js b/Sources/Navigator/EPUB/Scripts/src/fixed-page.js index f5bc1c12e..91121e589 100644 --- a/Sources/Navigator/EPUB/Scripts/src/fixed-page.js +++ b/Sources/Navigator/EPUB/Scripts/src/fixed-page.js @@ -4,14 +4,37 @@ // available in the top-level LICENSE file of the project. // +// Page layout types. +export const PageType = { + SINGLE: "single", + SPREAD_LEFT: "spread-left", + SPREAD_RIGHT: "spread-right", + SPREAD_CENTER: "spread-center", +}; + +// Fit modes for scaling content. +export const Fit = { + AUTO: "auto", + PAGE: "page", + WIDTH: "width", +}; + // Manages a fixed layout resource embedded in an iframe. -export function FixedPage(iframeId) { +// @param iframeId - ID of the iframe element +// @param pageType - Type of page layout from PageType enum +export function FixedPage(iframeId, pageType) { // Fixed dimensions for the page, extracted from the viewport meta tag. var _pageSize = null; // Available viewport size to fill with the resource. var _viewportSize = null; // Margins that should not overlap the content. var _safeAreaInsets = null; + // Fit mode for scaling the page. + var _fit = Fit.AUTO; + // Type of page layout (determines centering behavior). + var _pageType = Object.values(PageType).includes(pageType) + ? pageType + : PageType.SINGLE; // iFrame containing the page. var _iframe = document.getElementById(iframeId); @@ -42,7 +65,7 @@ export function FixedPage(iframeId) { } } - // Layouts the page iframe to center its content and scale it to fill the available viewport. + // Layouts the page iframe and scale it according to the current fit mode. function layoutPage() { if (!_pageSize || !_viewportSize || !_safeAreaInsets) { return; @@ -50,13 +73,57 @@ export function FixedPage(iframeId) { _iframe.style.width = _pageSize.width + "px"; _iframe.style.height = _pageSize.height + "px"; - _iframe.style.marginTop = - _safeAreaInsets.top - _safeAreaInsets.bottom + "px"; // Calculates the zoom scale required to fit the content to the viewport. var widthRatio = _viewportSize.width / _pageSize.width; var heightRatio = _viewportSize.height / _pageSize.height; - var scale = Math.min(widthRatio, heightRatio); + var scale; + + switch (_fit) { + case Fit.WIDTH: + // Fit to width only. + scale = widthRatio; + break; + // Auto is equivalent to page in paginated mode, we don't have a scroll mode for FXL. + case Fit.AUTO: + case Fit.PAGE: + default: + // Fit both dimensions. + scale = Math.min(widthRatio, heightRatio); + break; + } + + // Calculate the scaled height of the content + var scaledHeight = _pageSize.height * scale; + + // Determine the appropriate transform based on page type. + // Single page and center page in spread need horizontal centering. + // Left/right pages in spread don't need horizontal transform. + var needsHorizontalCenter = + _pageType === PageType.SINGLE || _pageType === PageType.SPREAD_CENTER; + + // For width fit, if content overflows vertically, align to top + // For page fit, center the content vertically + if (_fit === Fit.WIDTH && scaledHeight > _viewportSize.height) { + // Content overflows: align to top with safe area inset + // Override the CSS centering + _iframe.style.top = _safeAreaInsets.top + "px"; + if (needsHorizontalCenter) { + _iframe.style.transform = "translateX(-50%)"; + } else { + _iframe.style.transform = "none"; + } + } else { + // Content fits or is page fit: center vertically + // Keep the CSS centering but adjust for safe area insets + var verticalOffset = _safeAreaInsets.top - _safeAreaInsets.bottom; + _iframe.style.top = "calc(50% + " + verticalOffset + "px)"; + if (needsHorizontalCenter) { + _iframe.style.transform = "translate(-50%, -50%)"; + } else { + _iframe.style.transform = "translateY(-50%)"; + } + } // Sets the viewport of the wrapper page (this page) to scale the iframe. var viewport = document.querySelector("meta[name=viewport]"); @@ -123,9 +190,12 @@ export function FixedPage(iframeId) { }, // Updates the available viewport to display the resource. - setViewport: function (viewportSize, safeAreaInsets) { + setViewport: function (viewportSize, safeAreaInsets, fit) { _viewportSize = viewportSize; _safeAreaInsets = safeAreaInsets; + if (Object.values(Fit).includes(fit)) { + _fit = fit; + } layoutPage(); }, diff --git a/Sources/Navigator/EPUB/Scripts/src/index-fixed-wrapper-one.js b/Sources/Navigator/EPUB/Scripts/src/index-fixed-wrapper-one.js index ea0224dcb..f1f6f4306 100644 --- a/Sources/Navigator/EPUB/Scripts/src/index-fixed-wrapper-one.js +++ b/Sources/Navigator/EPUB/Scripts/src/index-fixed-wrapper-one.js @@ -6,9 +6,9 @@ // Script used for the single spread wrapper HTML page for fixed layout resources. -import { FixedPage } from "./fixed-page"; +import { FixedPage, PageType } from "./fixed-page"; -var page = FixedPage("page"); +var page = FixedPage("page", PageType.SINGLE); // Public API called from Swift. global.spread = { @@ -30,7 +30,7 @@ global.spread = { }, // Updates the available viewport to display the resources. - setViewport: function (viewportSize, safeAreaInsets) { - page.setViewport(viewportSize, safeAreaInsets); + setViewport: function (viewportSize, safeAreaInsets, fit) { + page.setViewport(viewportSize, safeAreaInsets, fit); }, }; diff --git a/Sources/Navigator/EPUB/Scripts/src/index-fixed-wrapper-two.js b/Sources/Navigator/EPUB/Scripts/src/index-fixed-wrapper-two.js index a2848eec9..3c31f660a 100644 --- a/Sources/Navigator/EPUB/Scripts/src/index-fixed-wrapper-two.js +++ b/Sources/Navigator/EPUB/Scripts/src/index-fixed-wrapper-two.js @@ -6,12 +6,12 @@ // Script used for the single spread wrapper HTML page for fixed layout resources. -import { FixedPage } from "./fixed-page"; +import { FixedPage, PageType } from "./fixed-page"; var pages = { - left: FixedPage("page-left"), - right: FixedPage("page-right"), - center: FixedPage("page-center"), + left: FixedPage("page-left", PageType.SPREAD_LEFT), + right: FixedPage("page-right", PageType.SPREAD_RIGHT), + center: FixedPage("page-center", PageType.SPREAD_CENTER), }; function forEachPage(callback) { @@ -76,28 +76,40 @@ global.spread = { }, // Updates the available viewport to display the resources. - setViewport: function (viewportSize, safeAreaInsets) { + setViewport: function (viewportSize, safeAreaInsets, fit) { viewportSize.width /= 2; - pages.left.setViewport(viewportSize, { - top: safeAreaInsets.top, - right: 0, - bottom: safeAreaInsets.bottom, - left: safeAreaInsets.left, - }); + pages.left.setViewport( + viewportSize, + { + top: safeAreaInsets.top, + right: 0, + bottom: safeAreaInsets.bottom, + left: safeAreaInsets.left, + }, + fit + ); - pages.right.setViewport(viewportSize, { - top: safeAreaInsets.top, - right: safeAreaInsets.right, - bottom: safeAreaInsets.bottom, - left: 0, - }); + pages.right.setViewport( + viewportSize, + { + top: safeAreaInsets.top, + right: safeAreaInsets.right, + bottom: safeAreaInsets.bottom, + left: 0, + }, + fit + ); - pages.center.setViewport(viewportSize, { - top: safeAreaInsets.top, - right: 0, - bottom: safeAreaInsets.bottom, - left: 0, - }); + pages.center.setViewport( + viewportSize, + { + top: safeAreaInsets.top, + right: 0, + bottom: safeAreaInsets.bottom, + left: 0, + }, + fit + ); }, }; diff --git a/TestApp/Sources/Reader/Common/Preferences/UserPreferences.swift b/TestApp/Sources/Reader/Common/Preferences/UserPreferences.swift index 214f88e29..381b215dc 100644 --- a/TestApp/Sources/Reader/Common/Preferences/UserPreferences.swift +++ b/TestApp/Sources/Reader/Common/Preferences/UserPreferences.swift @@ -123,6 +123,7 @@ struct UserPreferences< fixedLayoutUserPreferences( commit: commit, backgroundColor: editor.backgroundColor, + fit: editor.fit, language: editor.language, readingProgression: editor.readingProgression, spread: editor.spread