Convert web pages to EPUB. Send them to your e-reader. All over WiFi.
CrossX is a native SwiftUI app for iOS, iPadOS, and macOS that converts any web page into an EPUB 2.0 e-book and transfers it to an Xteink device e-reader over its local WiFi hotspot. No cloud services, no accounts, no subscriptions — just paste a URL, tap convert, and read.
Now available on the App Store — free to download.
The app supports both Stock and CrossPoint firmware variants with automatic device detection, includes a full on-device file manager, and ships with an iOS Share Extension so you can send pages directly from Safari.
Features · Screenshots · How It Works · Getting Started · Device Setup · Architecture · Roadmap
<h2> headings or by paragraph count (50 max per chapter)Data object via ZIPFoundationTitle - Author - domain - YYYY-MM-DD.epub192.168.3.3) and CrossPoint firmware (192.168.4.1 / crosspoint.local)crosspoint.local with static IP fallbackURLSessionUploadTask delegate.epub, .xtc, .bump, and .txt formats.glassEffect() modifiersUIPasteboard (iOS) and NSPasteboard (macOS)┌──────────────────────────────────────────────────────────┐
│ CrossX App │
│ │
│ URL ──► Fetch HTML ──► Extract Content ──► Sanitize │
│ │ │ │ │
│ │ SwiftSoup (fast) │ │
│ │ or │ │
│ │ Readability.js (fallback) │ │
│ │ or │ │
│ │ Twitter API (tweets) │ │
│ │ ▼ │
│ │ Build EPUB │
│ │ (in-memory ZIP) │
│ │ │ │
│ │ ┌──────────┴─────────┐│
│ │ │ ││
│ ▼ Device connected? ││
│ URLSession │ ││
│ ┌─────┴─────┐ ││
│ Yes No ││
│ │ │ ││
│ multipart Queue to disk ││
│ POST (send later) ││
└──────────────────────────┬───────────────┬──────────────┘│
│ │
WiFi Hotspot App Support/
│ EPUBQueue/
▼
┌─────────────────────────┐
│ Xteink X4 │
│ E-Reader │
│ │
│ Stock: 192.168.3.3 │
│ CrossPoint: 192.168.4.1 │
│ (or mDNS) │
└─────────────────────────┘
| Requirement | Version |
|---|---|
| Xcode | 26.0+ (beta) |
| iOS / iPadOS | 26.0+ |
| macOS | 26.0+ |
| Swift | 5 |
Note: This app targets the latest Apple platform SDKs. You need Xcode 26 beta or later to build.
git clone https://github.com/jtvargas/crosspoint-app.git
cd crosspoint-app
open SendToX4.xcodeproj
Xcode will automatically resolve Swift Package Manager dependencies (ZIPFoundation and SwiftSoup).
iOS Simulator:
xcodebuild -project SendToX4.xcodeproj \
-scheme SendToX4 \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro' \
build
macOS:
xcodebuild -project SendToX4.xcodeproj \
-scheme SendToX4 \
-destination 'platform=macOS' \
build
Or simply select your target device in Xcode and press Cmd+R.
See Device Setup below.
The Xteink X4 e-reader creates its own WiFi hotspot. CrossX communicates with it over plain HTTP on the local network.
The app will automatically detect which firmware your device is running:
| Firmware | IP Address | mDNS | Endpoints |
|---|---|---|---|
| Stock | 192.168.3.3 | — | /list, /edit |
| CrossPoint | 192.168.4.1 | crosspoint.local | /api/files, /upload, /mkdir, /delete |
Open Settings (gear icon) to:
Network note: The app requires the
NSAllowsLocalNetworkingATS exception andcom.apple.security.network.cliententitlement for plain HTTP communication with the device. These are already configured in the project.
CrossX follows MVVM (Model-View-ViewModel) with protocol-oriented services:
Views → ViewModels → Services
│ │
│ ├─ DeviceService (protocol)
│ │ ├─ StockFirmwareService
│ │ └─ CrossPointFirmwareService
│ │
│ ├─ ContentExtractor (SwiftSoup)
│ ├─ ReadabilityExtractor (WKWebView)
│ ├─ TwitterExtractor (fxtwitter API)
│ ├─ EPUBBuilder (ZIPFoundation)
│ └─ WebPageFetcher (URLSession)
│
└─ SwiftData Models
├─ Article (conversion history)
├─ DeviceSettings (configuration)
├─ ActivityEvent (file operations log)
└─ QueueItem (EPUB send queue)
DeviceService protocol with concrete implementations per firmware, enabling easy mocking and future firmware supportData objects directlyConvertURLIntent runs the full conversion pipeline without opening the app, using its own ModelContext against the shared SwiftData store@MainActor by default — the project uses SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; services are explicitly marked nonisolated to avoid stack overflowsSDKROOT = auto with #if os(iOS) / #if canImport(UIKit) conditional compilation (not Mac Catalyst)crosspoint-app/
├── SendToX4.xcodeproj/ # Xcode project (SPM dependencies, build settings)
├── Info.plist # ATS local networking exception
├── AGENTS.md # Developer reference (architecture, conventions, deep dives)
├── README.md # This file
├── LICENSE # MIT License
│
├── SendToX4/ # Main app target
│ ├── SendToX4App.swift # @main entry point, SwiftData ModelContainer setup
│ ├── SendToX4.entitlements # App Sandbox + network client + Siri
│ │
│ ├── Models/
│ │ ├── Article.swift # Conversion history model (URL, title, status, error)
│ │ ├── DeviceSettings.swift # Device config singleton (firmware type, IP, toggles)
│ │ ├── ActivityEvent.swift # File operation log (upload, mkdir, move, delete, queue)
│ │ └── QueueItem.swift # EPUB send queue model (file path, size, linked Article)
│ │
│ ├── Views/
│ │ ├── MainView.swift # Root tab view (Convert, History, File Manager, WallpaperX)
│ │ ├── ConvertView.swift # URL input, convert & send actions, share sheet
│ │ ├── HistoryView.swift # Unified activity timeline with filtering and search
│ │ ├── FileManagerView.swift # Device file browser with breadcrumbs
│ │ ├── FileManagerRow.swift # File/folder row with context menu
│ │ ├── SettingsSheet.swift # Device configuration form
│ │ ├── SettingsToolbarModifier.swift # Reusable gear button toolbar modifier
│ │ ├── DeviceStatusBar.swift # Device info bar (version, IP, RSSI, uptime)
│ │ ├── DeviceConnectionAccessory.swift # iOS bottom tab accessory (connect status)
│ │ ├── MacDeviceStatusBar.swift # macOS bottom status bar (Xcode-style)
│ │ ├── WallpaperXView.swift # Placeholder for future wallpaper feature
│ │ ├── MoveFileSheet.swift # Destination folder picker for move
│ │ ├── RenameFileSheet.swift # File rename with extension lock
│ │ └── CreateFolderSheet.swift # New folder name input with validation
│ │
│ ├── ViewModels/
│ │ ├── ConvertViewModel.swift # URL → EPUB → device pipeline orchestrator
│ │ ├── DeviceViewModel.swift # Connection state, auto-detection, upload progress
│ │ ├── FileManagerViewModel.swift # File browsing, CRUD operations, activity logging
│ │ ├── HistoryViewModel.swift # Search, delete, granular clear for history
│ │ └── QueueViewModel.swift # Queue management (enqueue, sendAll, remove, clear)
│ │
│ ├── Services/
│ │ ├── DeviceService.swift # Protocol + models (DeviceFile, DeviceStatus, DeviceError)
│ │ ├── StockFirmwareService.swift # Stock firmware implementation (192.168.3.3)
│ │ ├── CrossPointFirmwareService.swift # CrossPoint implementation (192.168.4.1 / mDNS)
│ │ ├── DeviceDiscovery.swift # Concurrent firmware auto-detection engine
│ │ ├── EPUBBuilder.swift # In-memory EPUB 2.0 ZIP builder
│ │ ├── EPUBTemplates.swift # EPUB XML templates (OPF, NCX, XHTML, CSS)
│ │ ├── ChapterSplitter.swift # Long content → multi-chapter splitting
│ │ ├── ContentExtractor.swift # SwiftSoup heuristic article extraction
│ │ ├── ReadabilityExtractor.swift # WKWebView + Readability.js fallback
│ │ ├── WebPageFetcher.swift # URLSession HTML fetcher with encoding detection
│ │ └── TwitterExtractor.swift # X/Twitter via fxtwitter API
│ │
│ ├── Intents/
│ │ ├── ConvertURLIntent.swift # App Intent: URL → EPUB → queue (Siri/Shortcuts)
│ │ └── CrossXShortcuts.swift # AppShortcutsProvider (Siri phrases)
│ │
│ ├── Utilities/
│ │ ├── HTMLSanitizer.swift # Strip unsafe HTML for text-only EPUB
│ │ ├── StringExtensions.swift # XML escaping, domain extraction, truncation
│ │ ├── FileNameGenerator.swift # EPUB filename from metadata
│ │ ├── ClipboardHelper.swift # Cross-platform clipboard (UIKit/AppKit)
│ │ ├── StorageCalculator.swift # Storage size calculations (DB, cache, queue, temp)
│ │ ├── ReviewPromptManager.swift # In-app review prompt after successful actions
│ │ └── DesignTokens.swift # AppColor design system (accent, success, error, warning)
│ │
│ ├── Resources/
│ │ └── readability.js # Mozilla Readability.js (bundled for WKWebView)
│ │
│ └── Assets.xcassets/ # App icon, AccentColor (teal light/dark)
│
└── SendToX4ShareExtension/ # iOS Share Extension target
├── Info.plist # Extension config (accepts 1 web URL)
└── ShareViewController.swift # Full pipeline: fetch → extract → EPUB → send/save
The conversion pipeline runs entirely in memory with no temporary files:
WebPageFetcher downloads the HTML via URLSession with a Safari user-agent, encoding detection, and redirect followingContentExtractor (SwiftSoup) parses the DOM for article content using semantic selectors (<article>, [role=main], .post-content, etc.). If extraction fails (< 400 chars), falls back to ReadabilityExtractor (WKWebView + Readability.js). Twitter/X URLs use TwitterExtractor via the fxtwitter APIHTMLSanitizer strips all scripts, styles, forms, media, images, SVGs, iframes, event handlers, and data attributes. Links are converted to plain text for a clean reading experienceEPUBBuilder assembles the EPUB 2.0 package in memory: mimetype (uncompressed), META-INF/container.xml, content.opf, toc.ncx, and one or more chapter-N.xhtml files. Long content is auto-split by ChapterSplitter at <h2> boundaries or every 50 paragraphsData blob is uploaded via multipart/form-data POST to the device's upload endpoint, with real-time progress trackingCrossX uses a tiered extraction approach to handle the widest range of web pages:
| Tier | Extractor | Method | When |
|---|---|---|---|
| 1 | TwitterExtractor | fxtwitter JSON API | Twitter/X status URLs |
| 2 | ContentExtractor | SwiftSoup DOM parsing | All other URLs (primary) |
| 3 | ReadabilityExtractor | WKWebView + Readability.js | Fallback when SwiftSoup extracts < 400 chars |
The SwiftSoup extractor uses a priority list of CSS selectors to find article content:
article, [role=main], .post-content, .entry-content, .article-body, #content, main, and more.
Metadata (title, author, description, language) is extracted from Open Graph tags, meta tags, and heading elements.
CrossX uses a minimal design token system with four semantic colors:
| Token | Color | Usage |
|---|---|---|
AppColor.accent | Teal | Primary actions, navigation, icons |
AppColor.success | Green | Successful operations, connected state |
AppColor.error | Red | Errors, destructive actions, disconnected state |
AppColor.warning | Orange | Warnings, pending states |
The AccentColor asset is set to teal with light and dark mode variants. The UI uses iOS 26 / macOS 26 Liquid Glass modifiers (.glassEffect()) for a translucent, modern appearance.
| Package | Version | Purpose |
|---|---|---|
| ZIPFoundation | >= 0.9.0 | In-memory EPUB ZIP archive creation |
| SwiftSoup | >= 2.6.0 | HTML parsing and content extraction |
Dependencies are managed via Xcode's Swift Package Manager integration. They resolve automatically when you open the project.
Contributions are welcome! See CONTRIBUTING.md for the full guide, including:
Quick start:
git checkout -b feature/my-feature)xcodebuild for iOS and macOSThis project is licensed under the MIT License — see the LICENSE file for details.
Built for the Xteink X4 e-reader community.
Swift
99.5%
Convert web pages to EPUB. Send them to your e-reader. All over WiFi.
CrossX is a native SwiftUI app for iOS, iPadOS, and macOS that converts any web page into an EPUB 2.0 e-book and transfers it to an Xteink device e-reader over its local WiFi hotspot. No cloud services, no accounts, no subscriptions — just paste a URL, tap convert, and read.
Now available on the App Store — free to download.
The app supports both Stock and CrossPoint firmware variants with automatic device detection, includes a full on-device file manager, and ships with an iOS Share Extension so you can send pages directly from Safari.
Features · Screenshots · How It Works · Getting Started · Device Setup · Architecture · Roadmap
<h2> headings or by paragraph count (50 max per chapter)Data object via ZIPFoundationTitle - Author - domain - YYYY-MM-DD.epub192.168.3.3) and CrossPoint firmware (192.168.4.1 / crosspoint.local)crosspoint.local with static IP fallbackURLSessionUploadTask delegate.epub, .xtc, .bump, and .txt formats.glassEffect() modifiersUIPasteboard (iOS) and NSPasteboard (macOS)┌──────────────────────────────────────────────────────────┐
│ CrossX App │
│ │
│ URL ──► Fetch HTML ──► Extract Content ──► Sanitize │
│ │ │ │ │
│ │ SwiftSoup (fast) │ │
│ │ or │ │
│ │ Readability.js (fallback) │ │
│ │ or │ │
│ │ Twitter API (tweets) │ │
│ │ ▼ │
│ │ Build EPUB │
│ │ (in-memory ZIP) │
│ │ │ │
│ │ ┌──────────┴─────────┐│
│ │ │ ││
│ ▼ Device connected? ││
│ URLSession │ ││
│ ┌─────┴─────┐ ││
│ Yes No ││
│ │ │ ││
│ multipart Queue to disk ││
│ POST (send later) ││
└──────────────────────────┬───────────────┬──────────────┘│
│ │
WiFi Hotspot App Support/
│ EPUBQueue/
▼
┌─────────────────────────┐
│ Xteink X4 │
│ E-Reader │
│ │
│ Stock: 192.168.3.3 │
│ CrossPoint: 192.168.4.1 │
│ (or mDNS) │
└─────────────────────────┘
| Requirement | Version |
|---|---|
| Xcode | 26.0+ (beta) |
| iOS / iPadOS | 26.0+ |
| macOS | 26.0+ |
| Swift | 5 |
Note: This app targets the latest Apple platform SDKs. You need Xcode 26 beta or later to build.
git clone https://github.com/jtvargas/crosspoint-app.git
cd crosspoint-app
open SendToX4.xcodeproj
Xcode will automatically resolve Swift Package Manager dependencies (ZIPFoundation and SwiftSoup).
iOS Simulator:
xcodebuild -project SendToX4.xcodeproj \
-scheme SendToX4 \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro' \
build
macOS:
xcodebuild -project SendToX4.xcodeproj \
-scheme SendToX4 \
-destination 'platform=macOS' \
build
Or simply select your target device in Xcode and press Cmd+R.
See Device Setup below.
The Xteink X4 e-reader creates its own WiFi hotspot. CrossX communicates with it over plain HTTP on the local network.
The app will automatically detect which firmware your device is running:
| Firmware | IP Address | mDNS | Endpoints |
|---|---|---|---|
| Stock | 192.168.3.3 | — | /list, /edit |
| CrossPoint | 192.168.4.1 | crosspoint.local | /api/files, /upload, /mkdir, /delete |
Open Settings (gear icon) to:
Network note: The app requires the
NSAllowsLocalNetworkingATS exception andcom.apple.security.network.cliententitlement for plain HTTP communication with the device. These are already configured in the project.
CrossX follows MVVM (Model-View-ViewModel) with protocol-oriented services:
Views → ViewModels → Services
│ │
│ ├─ DeviceService (protocol)
│ │ ├─ StockFirmwareService
│ │ └─ CrossPointFirmwareService
│ │
│ ├─ ContentExtractor (SwiftSoup)
│ ├─ ReadabilityExtractor (WKWebView)
│ ├─ TwitterExtractor (fxtwitter API)
│ ├─ EPUBBuilder (ZIPFoundation)
│ └─ WebPageFetcher (URLSession)
│
└─ SwiftData Models
├─ Article (conversion history)
├─ DeviceSettings (configuration)
├─ ActivityEvent (file operations log)
└─ QueueItem (EPUB send queue)
DeviceService protocol with concrete implementations per firmware, enabling easy mocking and future firmware supportData objects directlyConvertURLIntent runs the full conversion pipeline without opening the app, using its own ModelContext against the shared SwiftData store@MainActor by default — the project uses SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; services are explicitly marked nonisolated to avoid stack overflowsSDKROOT = auto with #if os(iOS) / #if canImport(UIKit) conditional compilation (not Mac Catalyst)crosspoint-app/
├── SendToX4.xcodeproj/ # Xcode project (SPM dependencies, build settings)
├── Info.plist # ATS local networking exception
├── AGENTS.md # Developer reference (architecture, conventions, deep dives)
├── README.md # This file
├── LICENSE # MIT License
│
├── SendToX4/ # Main app target
│ ├── SendToX4App.swift # @main entry point, SwiftData ModelContainer setup
│ ├── SendToX4.entitlements # App Sandbox + network client + Siri
│ │
│ ├── Models/
│ │ ├── Article.swift # Conversion history model (URL, title, status, error)
│ │ ├── DeviceSettings.swift # Device config singleton (firmware type, IP, toggles)
│ │ ├── ActivityEvent.swift # File operation log (upload, mkdir, move, delete, queue)
│ │ └── QueueItem.swift # EPUB send queue model (file path, size, linked Article)
│ │
│ ├── Views/
│ │ ├── MainView.swift # Root tab view (Convert, History, File Manager, WallpaperX)
│ │ ├── ConvertView.swift # URL input, convert & send actions, share sheet
│ │ ├── HistoryView.swift # Unified activity timeline with filtering and search
│ │ ├── FileManagerView.swift # Device file browser with breadcrumbs
│ │ ├── FileManagerRow.swift # File/folder row with context menu
│ │ ├── SettingsSheet.swift # Device configuration form
│ │ ├── SettingsToolbarModifier.swift # Reusable gear button toolbar modifier
│ │ ├── DeviceStatusBar.swift # Device info bar (version, IP, RSSI, uptime)
│ │ ├── DeviceConnectionAccessory.swift # iOS bottom tab accessory (connect status)
│ │ ├── MacDeviceStatusBar.swift # macOS bottom status bar (Xcode-style)
│ │ ├── WallpaperXView.swift # Placeholder for future wallpaper feature
│ │ ├── MoveFileSheet.swift # Destination folder picker for move
│ │ ├── RenameFileSheet.swift # File rename with extension lock
│ │ └── CreateFolderSheet.swift # New folder name input with validation
│ │
│ ├── ViewModels/
│ │ ├── ConvertViewModel.swift # URL → EPUB → device pipeline orchestrator
│ │ ├── DeviceViewModel.swift # Connection state, auto-detection, upload progress
│ │ ├── FileManagerViewModel.swift # File browsing, CRUD operations, activity logging
│ │ ├── HistoryViewModel.swift # Search, delete, granular clear for history
│ │ └── QueueViewModel.swift # Queue management (enqueue, sendAll, remove, clear)
│ │
│ ├── Services/
│ │ ├── DeviceService.swift # Protocol + models (DeviceFile, DeviceStatus, DeviceError)
│ │ ├── StockFirmwareService.swift # Stock firmware implementation (192.168.3.3)
│ │ ├── CrossPointFirmwareService.swift # CrossPoint implementation (192.168.4.1 / mDNS)
│ │ ├── DeviceDiscovery.swift # Concurrent firmware auto-detection engine
│ │ ├── EPUBBuilder.swift # In-memory EPUB 2.0 ZIP builder
│ │ ├── EPUBTemplates.swift # EPUB XML templates (OPF, NCX, XHTML, CSS)
│ │ ├── ChapterSplitter.swift # Long content → multi-chapter splitting
│ │ ├── ContentExtractor.swift # SwiftSoup heuristic article extraction
│ │ ├── ReadabilityExtractor.swift # WKWebView + Readability.js fallback
│ │ ├── WebPageFetcher.swift # URLSession HTML fetcher with encoding detection
│ │ └── TwitterExtractor.swift # X/Twitter via fxtwitter API
│ │
│ ├── Intents/
│ │ ├── ConvertURLIntent.swift # App Intent: URL → EPUB → queue (Siri/Shortcuts)
│ │ └── CrossXShortcuts.swift # AppShortcutsProvider (Siri phrases)
│ │
│ ├── Utilities/
│ │ ├── HTMLSanitizer.swift # Strip unsafe HTML for text-only EPUB
│ │ ├── StringExtensions.swift # XML escaping, domain extraction, truncation
│ │ ├── FileNameGenerator.swift # EPUB filename from metadata
│ │ ├── ClipboardHelper.swift # Cross-platform clipboard (UIKit/AppKit)
│ │ ├── StorageCalculator.swift # Storage size calculations (DB, cache, queue, temp)
│ │ ├── ReviewPromptManager.swift # In-app review prompt after successful actions
│ │ └── DesignTokens.swift # AppColor design system (accent, success, error, warning)
│ │
│ ├── Resources/
│ │ └── readability.js # Mozilla Readability.js (bundled for WKWebView)
│ │
│ └── Assets.xcassets/ # App icon, AccentColor (teal light/dark)
│
└── SendToX4ShareExtension/ # iOS Share Extension target
├── Info.plist # Extension config (accepts 1 web URL)
└── ShareViewController.swift # Full pipeline: fetch → extract → EPUB → send/save
The conversion pipeline runs entirely in memory with no temporary files:
WebPageFetcher downloads the HTML via URLSession with a Safari user-agent, encoding detection, and redirect followingContentExtractor (SwiftSoup) parses the DOM for article content using semantic selectors (<article>, [role=main], .post-content, etc.). If extraction fails (< 400 chars), falls back to ReadabilityExtractor (WKWebView + Readability.js). Twitter/X URLs use TwitterExtractor via the fxtwitter APIHTMLSanitizer strips all scripts, styles, forms, media, images, SVGs, iframes, event handlers, and data attributes. Links are converted to plain text for a clean reading experienceEPUBBuilder assembles the EPUB 2.0 package in memory: mimetype (uncompressed), META-INF/container.xml, content.opf, toc.ncx, and one or more chapter-N.xhtml files. Long content is auto-split by ChapterSplitter at <h2> boundaries or every 50 paragraphsData blob is uploaded via multipart/form-data POST to the device's upload endpoint, with real-time progress trackingCrossX uses a tiered extraction approach to handle the widest range of web pages:
| Tier | Extractor | Method | When |
|---|---|---|---|
| 1 | TwitterExtractor | fxtwitter JSON API | Twitter/X status URLs |
| 2 | ContentExtractor | SwiftSoup DOM parsing | All other URLs (primary) |
| 3 | ReadabilityExtractor | WKWebView + Readability.js | Fallback when SwiftSoup extracts < 400 chars |
The SwiftSoup extractor uses a priority list of CSS selectors to find article content:
article, [role=main], .post-content, .entry-content, .article-body, #content, main, and more.
Metadata (title, author, description, language) is extracted from Open Graph tags, meta tags, and heading elements.
CrossX uses a minimal design token system with four semantic colors:
| Token | Color | Usage |
|---|---|---|
AppColor.accent | Teal | Primary actions, navigation, icons |
AppColor.success | Green | Successful operations, connected state |
AppColor.error | Red | Errors, destructive actions, disconnected state |
AppColor.warning | Orange | Warnings, pending states |
The AccentColor asset is set to teal with light and dark mode variants. The UI uses iOS 26 / macOS 26 Liquid Glass modifiers (.glassEffect()) for a translucent, modern appearance.
| Package | Version | Purpose |
|---|---|---|
| ZIPFoundation | >= 0.9.0 | In-memory EPUB ZIP archive creation |
| SwiftSoup | >= 2.6.0 | HTML parsing and content extraction |
Dependencies are managed via Xcode's Swift Package Manager integration. They resolve automatically when you open the project.
Contributions are welcome! See CONTRIBUTING.md for the full guide, including:
Quick start:
git checkout -b feature/my-feature)xcodebuild for iOS and macOSThis project is licensed under the MIT License — see the LICENSE file for details.
Built for the Xteink X4 e-reader community.
Swift
99.5%