Compare commits
8
Commits
7fca7fbfcd
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
218ea4b424 | ||
|
|
e0347c8051 | ||
|
|
b5687146f0 | ||
|
|
31a0f61fa3 | ||
|
|
6fcf05a289 | ||
|
|
9500c353a3 | ||
|
|
aef3cc3409 | ||
|
|
041b9b9567 |
@@ -10,14 +10,18 @@ SOURCES += \
|
||||
src/apiclient.cpp \
|
||||
src/downloader.cpp \
|
||||
src/opdsparser.cpp \
|
||||
src/epub.cpp
|
||||
src/epub.cpp \
|
||||
src/readerstate.cpp \
|
||||
src/pagedtextitem.cpp
|
||||
|
||||
HEADERS += \
|
||||
src/settings.h \
|
||||
src/apiclient.h \
|
||||
src/downloader.h \
|
||||
src/opdsparser.h \
|
||||
src/epub.h
|
||||
src/epub.h \
|
||||
src/readerstate.h \
|
||||
src/pagedtextitem.h
|
||||
|
||||
LIBS += -lz
|
||||
|
||||
@@ -33,8 +37,9 @@ DISTFILES += $$OTHER_FILES
|
||||
|
||||
# Translations are NOT handled by the sailfishapp_i18n template on the 5.1 kit
|
||||
# (the .qm is never generated/installed reliably). The spec builds the .qm
|
||||
# with lrelease and installs it into /usr/share/translations directly;
|
||||
# libsailfishapp loads harbour-calibreweb_<locale>.qm automatically.
|
||||
# with lrelease and installs it into the app data dir
|
||||
# (/usr/share/harbour-calibreweb/translations — the RPM validator rejects
|
||||
# /usr/share/translations); main.cpp loads it explicitly (see language block).
|
||||
# Source strings are ENGLISH (default language); harbour-calibreweb_it.ts
|
||||
# provides the Italian catalogue.
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import QtQuick 2.0
|
||||
import Sailfish.Silica 1.0
|
||||
|
||||
// Cover: se un libro è aperto (readerState attivo dal ReaderPage) mostra
|
||||
// titolo e posizione di lettura, altrimenti icona e nome dell'app.
|
||||
CoverBackground {
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - 2 * Theme.paddingLarge
|
||||
spacing: Theme.paddingMedium
|
||||
visible: !readerState.active
|
||||
|
||||
Image {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
@@ -20,4 +24,29 @@ CoverBackground {
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - 2 * Theme.paddingLarge
|
||||
spacing: Theme.paddingSmall
|
||||
visible: readerState.active
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
text: readerState.title
|
||||
color: Theme.primaryColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
truncationMode: TruncationMode.Elide
|
||||
}
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
text: readerState.subtitle
|
||||
color: Theme.secondaryColor
|
||||
font.pixelSize: Theme.fontSizeExtraSmall
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
truncationMode: TruncationMode.Elide
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ ApplicationWindow {
|
||||
initialPage: Component { MainPage {} }
|
||||
cover: Component { CoverPage {} }
|
||||
|
||||
allowedOrientations: Orientation.Portrait
|
||||
_defaultPageOrientations: Orientation.Portrait
|
||||
// Niente vincoli a livello window: ogni pagina dichiara le proprie
|
||||
// orientazioni (il lettore ruota in landscape, le pagine di lista
|
||||
// restano in portrait). _defaultPageOrientations è API privata.
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import Sailfish.Silica 1.0
|
||||
Page {
|
||||
id: page
|
||||
|
||||
allowedOrientations: Orientation.Portrait
|
||||
|
||||
property var bookData: ({})
|
||||
property string coverSource: ""
|
||||
property bool downloading: false
|
||||
|
||||
@@ -4,6 +4,8 @@ import Sailfish.Silica 1.0
|
||||
Page {
|
||||
id: page
|
||||
|
||||
allowedOrientations: Orientation.Portrait
|
||||
|
||||
property string feedUrl
|
||||
property string pageTitle
|
||||
property string nextUrl: ""
|
||||
|
||||
@@ -4,6 +4,8 @@ import Sailfish.Silica 1.0
|
||||
Page {
|
||||
id: page
|
||||
|
||||
allowedOrientations: Orientation.Portrait
|
||||
|
||||
property bool loading: false
|
||||
property string errorMessage: ""
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ Dialog {
|
||||
id: dialog
|
||||
|
||||
property int marginLr: 24
|
||||
property int marginTb: 0
|
||||
property int marginTb: 24
|
||||
// funzione chiamata a ogni variazione: previewHandler(marginLr, marginTb)
|
||||
property var previewHandler: null
|
||||
|
||||
|
||||
+128
-165
@@ -2,14 +2,14 @@ import QtQuick 2.0
|
||||
import Sailfish.Silica 1.0
|
||||
import harbour.calibreweb 1.0
|
||||
|
||||
// Lettore senza WebView: il capitolo è rich text Qt in un Text QML.
|
||||
// STRUTTURA A PAGINE RIGIDE:
|
||||
// - SilicaFlickable ESTERNO con contentHeight = height: NON scrollabile,
|
||||
// serve solo per i pulley menu (PullDown/PushUp) che funzionano dal bordo
|
||||
// - Flickable INTERNO con interactive:false: il dito non lo scrolla MAI,
|
||||
// contentY cambia solo col tap (paginazione programmatica)
|
||||
// Risultato: niente scroll libero, niente snap, menu sempre funzionanti,
|
||||
// paginazione a tap affidabile al 100% (proprietà QML native).
|
||||
// Lettore a pagine REALI: il capitolo (rich text Qt) viene impaginato da
|
||||
// PagedTextItem (QTextDocument + QPainter, pattern QQuickPaintedItem come
|
||||
// harbour-books) in pagine misurate dal layout: niente scroll, niente stime,
|
||||
// niente testo che sborda e l'ultima pagina e' allineata da sola.
|
||||
// La finestra di lettura e' la GEOMETRIA dell'item: i margini (laterali,
|
||||
// alto/basso) e l'inset del notch valgono su OGNI pagina.
|
||||
// La posizione di lettura e' un indice di carattere: sopravvive al reflow del
|
||||
// font e alla rotazione senza timer ne' stime.
|
||||
Page {
|
||||
id: page
|
||||
|
||||
@@ -18,12 +18,11 @@ Page {
|
||||
property string bookFormat: "epub"
|
||||
|
||||
property int currentChapter: 0
|
||||
property double chapterFraction: 0.0
|
||||
// posizione di lettura nel capitolo corrente, stimata in caratteri di testo
|
||||
// dalla frazione di scroll (stabile col reflow del font)
|
||||
// posizione di lettura nel capitolo corrente in caratteri di testo
|
||||
// (metrica del capitolo, coerente coi segnalibri salvati)
|
||||
property int charOffset: 0
|
||||
// offset da ripristinare al cambio capitolo (-1 = nessuno)
|
||||
property int restoreCharOffset: -1
|
||||
// caratteri da ripristinare nel capitolo appena aperto (-1 = nessuno)
|
||||
property int pendingChars: -1
|
||||
// ultimo offset assoluto salvato (evita scritture continue)
|
||||
property int lastSavedAbs: -1
|
||||
// pagina corrente / pagine totali del capitolo (per la label del menu)
|
||||
@@ -31,17 +30,27 @@ Page {
|
||||
property int pageCount: 1
|
||||
property bool ready: false
|
||||
property string errorMessage: ""
|
||||
// reflow: scala il font (il testo si ri-avvolge), persistita in appSettings
|
||||
// reflow: scala il font (il testo si ri-impagina), persistita in appSettings
|
||||
property double fontScale: appSettings.readerFontScale
|
||||
// restore in sospeso: frazione da applicare quando il testo si ri-impagina
|
||||
property bool pendingRestore: false
|
||||
property double restoreFrac: 0.0
|
||||
// anteprima margini dal dialog (valori temporanei, -1 = usa le impostazioni)
|
||||
property int previewMarginL: -1
|
||||
property int previewMarginT: -1
|
||||
|
||||
allowedOrientations: Orientation.All
|
||||
|
||||
// Inset superiore per display con notch (Jolla Phone 2026): la finestra
|
||||
// dell'app inizia sopra la fine del ritaglio, quindi in portrait la zona di
|
||||
// lettura parte piu' in basso di un'altezza barra di stato, e il testo non
|
||||
// finisce mai sotto il notch (in landscape il notch e' laterale: gestito
|
||||
// dai margini orizzontali). Theme.statusBarHeight esiste nelle Silica
|
||||
// recenti: se manca si usa un valore prudente.
|
||||
function statusBarInset() {
|
||||
var h = Theme.statusBarHeight
|
||||
return (typeof h === "number" && h > 0) ? h : 48
|
||||
}
|
||||
|
||||
property int notchInset: page.isPortrait ? page.statusBarInset() : 0
|
||||
|
||||
EpubBook {
|
||||
id: book
|
||||
}
|
||||
@@ -143,43 +152,33 @@ Page {
|
||||
}
|
||||
}
|
||||
|
||||
// contenuto: Flickable NON interattivo (lo scroll è solo programmatico)
|
||||
Flickable {
|
||||
id: innerFlick
|
||||
// Motore di paginazione: l'item impagina il rich text del capitolo e
|
||||
// disegna la pagina corrente. I margini sono anchors: la finestra di
|
||||
// lettura vive DENTRO i margini, quindi il testo non puo' uscirne.
|
||||
PagedTextItem {
|
||||
id: reader
|
||||
anchors.fill: parent
|
||||
contentHeight: readerContent.height
|
||||
interactive: false
|
||||
clip: true
|
||||
|
||||
Item {
|
||||
id: readerContent
|
||||
width: parent.width
|
||||
height: Math.max(parent.height,
|
||||
textItem.height + 2 * (page.previewMarginT >= 0
|
||||
anchors.leftMargin: page.previewMarginL >= 0
|
||||
? page.previewMarginL : appSettings.readerMargin
|
||||
anchors.rightMargin: page.previewMarginL >= 0
|
||||
? page.previewMarginL : appSettings.readerMargin
|
||||
anchors.topMargin: page.notchInset + (page.previewMarginT >= 0
|
||||
? page.previewMarginT
|
||||
: appSettings.readerMarginV))
|
||||
|
||||
Text {
|
||||
id: textItem
|
||||
x: page.previewMarginL >= 0 ? page.previewMarginL : appSettings.readerMargin
|
||||
y: page.previewMarginT >= 0 ? page.previewMarginT : appSettings.readerMarginV
|
||||
width: parent.width - 2 * (page.previewMarginL >= 0
|
||||
? page.previewMarginL
|
||||
: appSettings.readerMargin)
|
||||
text: book.chapterRichText(page.currentChapter)
|
||||
textFormat: Text.RichText
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
font.pixelSize: page.fontPx()
|
||||
color: Theme.primaryColor
|
||||
onContentHeightChanged: page.onTextReflowed()
|
||||
}
|
||||
}
|
||||
: appSettings.readerMarginV)
|
||||
anchors.bottomMargin: page.previewMarginT >= 0
|
||||
? page.previewMarginT : appSettings.readerMarginV
|
||||
fontFamily: Theme.fontFamily
|
||||
fontPixelSize: page.fontPx()
|
||||
textColor: Theme.primaryColor
|
||||
// il reflow (font, geometria) mantiene la posizione internamente:
|
||||
// qui si aggiornano solo label, cover e segnalibro
|
||||
onPageChanged: page.syncState()
|
||||
onLayoutChanged: page.syncState()
|
||||
}
|
||||
|
||||
// Paginazione a tap: la meta' destra va avanti di una pagina, la
|
||||
// sinistra indietro. Le fasce superiore e inferiore sono lasciate ai
|
||||
// menu (PullDown in alto, PushUp in basso). NIENTE preventStealing:
|
||||
// il drag non ha effetti perche' il contenuto non e' scrollabile.
|
||||
// menu (PullDown in alto, PushUp in basso).
|
||||
MouseArea {
|
||||
id: tapZone
|
||||
anchors.fill: parent
|
||||
@@ -208,28 +207,28 @@ Page {
|
||||
onTriggered: pageStack.pop(null)
|
||||
}
|
||||
|
||||
// rete di sicurezza per il restore: il caso normale è applicato da
|
||||
// onTextReflowed appena il reflow cambia contentHeight; questo timer
|
||||
// copre i rari casi in cui l'altezza non cambia (capitoli identici).
|
||||
// Ritardo lungo: il reflow rich text può richiedere qualche centinaio di ms.
|
||||
Timer {
|
||||
id: restoreTimer
|
||||
interval: 500
|
||||
onTriggered: {
|
||||
if (page.pendingRestore) {
|
||||
page.pendingRestore = false
|
||||
applyRestore()
|
||||
}
|
||||
}
|
||||
// posizione di lettura nella metrica del capitolo (0..1), dal motore
|
||||
function readerFrac() {
|
||||
var n = reader.characterCount
|
||||
return n > 0 ? reader.position / n : 0.0
|
||||
}
|
||||
|
||||
// tracciamento periodico (aggiorna label e bookmark anche senza tap)
|
||||
Timer {
|
||||
id: trackTimer
|
||||
interval: 3000
|
||||
running: page.ready
|
||||
repeat: true
|
||||
onTriggered: readScrollPosition()
|
||||
// converte un offset di caratteri (metrica capitolo) nella posizione del
|
||||
// motore: le due metriche differiscono di poco, la frazione le allinea
|
||||
function posForOffset(off) {
|
||||
var len = book.chapterTextLength(page.currentChapter)
|
||||
var n = reader.characterCount
|
||||
if (len <= 0 || n <= 0)
|
||||
return 0
|
||||
if (off >= len)
|
||||
return Math.max(0, n - 1)
|
||||
return Math.round((off / len) * n)
|
||||
}
|
||||
|
||||
// offset di caratteri (metrica capitolo) della pagina corrente: e' quello
|
||||
// che finisce nel segnalibro, coerente con le chiavi salvate (v2)
|
||||
function offsetForPos() {
|
||||
return Math.round(readerFrac() * book.chapterTextLength(page.currentChapter))
|
||||
}
|
||||
|
||||
function overallProgress() {
|
||||
@@ -242,60 +241,24 @@ Page {
|
||||
return (start + page.charOffset) / total
|
||||
}
|
||||
|
||||
// frazione di contenuto per un offset in caratteri (off >= len = fondo)
|
||||
function offsetToFrac(off, chapterIndex) {
|
||||
var len = book.chapterTextLength(chapterIndex)
|
||||
if (len <= 0)
|
||||
return 0
|
||||
if (off >= len)
|
||||
return 1.0
|
||||
return Math.max(0, Math.min(1, off / len))
|
||||
}
|
||||
|
||||
function applyRestore() {
|
||||
var max = Math.max(0, innerFlick.contentHeight - innerFlick.height)
|
||||
innerFlick.contentY = page.restoreFrac * max
|
||||
readScrollPosition()
|
||||
}
|
||||
|
||||
// il testo si è ri-impaginato (cambio capitolo, font, margini):
|
||||
// applica la frazione di restore in sospeso
|
||||
function onTextReflowed() {
|
||||
if (page.pendingRestore) {
|
||||
page.pendingRestore = false
|
||||
applyRestore()
|
||||
}
|
||||
}
|
||||
|
||||
function readScrollPosition() {
|
||||
// aggiorna label, cover e segnalibro dopo un cambio pagina o un reflow
|
||||
function syncState() {
|
||||
if (!page.ready)
|
||||
return
|
||||
// passo di pagina = viewport - 2*margine verticale: ogni pagina mostra
|
||||
// il testo DENTRO i margini (niente righe a filo del bordo)
|
||||
var step = Math.max(1, innerFlick.height - 2 * page.marginV())
|
||||
var max = Math.max(0, innerFlick.contentHeight - innerFlick.height)
|
||||
var y = Math.max(0, Math.min(max, innerFlick.contentY))
|
||||
var len = book.chapterTextLength(page.currentChapter)
|
||||
page.chapterFraction = max > 0 ? y / max : 0
|
||||
// posizione per caratteri (stima dalla frazione: stabile col reflow)
|
||||
page.charOffset = Math.round(page.chapterFraction * len)
|
||||
page.pageIndex = Math.floor(y / step) + 1
|
||||
page.pageCount = Math.max(1, Math.ceil(max / step) + 1)
|
||||
page.pageIndex = reader.page + 1
|
||||
page.pageCount = reader.pageCount
|
||||
page.charOffset = offsetForPos()
|
||||
updateCoverState()
|
||||
maybeSave(false)
|
||||
}
|
||||
|
||||
// margine verticale effettivo (anteprima dialog oppure impostazioni)
|
||||
function marginV() {
|
||||
return page.previewMarginT >= 0 ? page.previewMarginT : appSettings.readerMarginV
|
||||
}
|
||||
|
||||
function maybeSave(force) {
|
||||
if (bookId <= 0)
|
||||
return
|
||||
// posizione assoluta nel libro = capitolo + caratteri del capitolo
|
||||
var abs = book.chapterStartChar(page.currentChapter) + page.charOffset
|
||||
// salva subito su forzatura, altrimenti solo se la posizione è cambiata
|
||||
// di almeno 20 caratteri (evita scritture continue a tracker attivo)
|
||||
// di almeno 20 caratteri (evita scritture continue)
|
||||
if (force || Math.abs(abs - page.lastSavedAbs) > 20) {
|
||||
page.lastSavedAbs = abs
|
||||
appSettings.setBookmarkKey(page.bookId, page.bookFormat,
|
||||
@@ -308,74 +271,52 @@ Page {
|
||||
return
|
||||
if (index !== page.currentChapter) {
|
||||
maybeSave(true)
|
||||
page.chapterFraction = 0
|
||||
page.charOffset = 0
|
||||
page.lastSavedAbs = -1
|
||||
}
|
||||
page.currentChapter = index
|
||||
// il binding del Text si aggiorna da solo; restore all'inizio del capitolo
|
||||
page.pendingRestore = true
|
||||
page.restoreFrac = page.restoreCharOffset >= 0
|
||||
? offsetToFrac(page.restoreCharOffset, page.currentChapter)
|
||||
: 0.0
|
||||
page.restoreCharOffset = -1
|
||||
restoreTimer.start()
|
||||
page.pendingChars = -1
|
||||
// carica il capitolo e vai all'inizio (posizione esplicita)
|
||||
reader.html = book.chapterRichText(index)
|
||||
reader.goToPosition(0)
|
||||
syncState()
|
||||
}
|
||||
|
||||
function changeFontScale(delta) {
|
||||
// limite ampio e passo 0.1: tanti ingrandimenti prima del limite
|
||||
page.fontScale = Math.max(0.7, Math.min(3.5, page.fontScale + delta))
|
||||
// persisti la scala: riparte da qui alla prossima apertura
|
||||
appSettings.readerFontScale = page.fontScale
|
||||
// il binding aggiorna il font; ripristina la posizione per CARATTERI
|
||||
// (stabile col reflow, a differenza della frazione di scroll: con il
|
||||
// testo più alto la stessa frazione mostrerebbe un punto diverso)
|
||||
page.restoreFrac = offsetToFrac(page.charOffset, page.currentChapter)
|
||||
page.pendingRestore = true
|
||||
restoreTimer.start()
|
||||
appSettings.readerFontScale = Math.max(0.7, Math.min(3.5, page.fontScale + delta))
|
||||
// il binding aggiorna fontPixelSize: il motore si ri-impagina e
|
||||
// mantiene da solo la posizione per carattere
|
||||
}
|
||||
|
||||
// pagina successiva: una pagina di testo = viewport meno i margini verticali
|
||||
// (così ogni pagina ha il testo dentro i margini, non a filo del bordo);
|
||||
// a fine capitolo passa al successivo
|
||||
// pagina successiva: a fine capitolo passa al successivo
|
||||
function nextPage() {
|
||||
if (!page.ready)
|
||||
return
|
||||
var step = Math.max(1, innerFlick.height - 2 * page.marginV())
|
||||
var max = Math.max(0, innerFlick.contentHeight - innerFlick.height)
|
||||
var y = innerFlick.contentY
|
||||
if (y >= max - 2) {
|
||||
// fine capitolo (o capitolo non scrollabile): segna la coda e passa
|
||||
if (reader.page >= reader.pageCount - 1) {
|
||||
// fine capitolo: segna la coda e passa al capitolo dopo
|
||||
page.charOffset = Math.max(0, book.chapterTextLength(page.currentChapter) - 1)
|
||||
maybeSave(true)
|
||||
gotoChapter(page.currentChapter + 1)
|
||||
} else {
|
||||
innerFlick.contentY = Math.min(max, y + step)
|
||||
readScrollPosition()
|
||||
reader.nextPage()
|
||||
}
|
||||
}
|
||||
|
||||
// pagina precedente: una pagina indietro; dall'inizio del capitolo va al
|
||||
// capitolo prima SULL'ULTIMA PAGINA (non salta la coda del capitolo)
|
||||
// pagina precedente: dall'inizio del capitolo va al capitolo prima
|
||||
// SULL'ULTIMA PAGINA (non salta la coda del capitolo)
|
||||
function prevPage() {
|
||||
if (!page.ready)
|
||||
return
|
||||
var step = Math.max(1, innerFlick.height - 2 * page.marginV())
|
||||
var y = innerFlick.contentY
|
||||
if (y > 1) {
|
||||
innerFlick.contentY = Math.max(0, y - step)
|
||||
readScrollPosition()
|
||||
if (reader.page > 0) {
|
||||
reader.previousPage()
|
||||
} else if (page.currentChapter > 0) {
|
||||
maybeSave(true)
|
||||
page.currentChapter -= 1
|
||||
page.chapterFraction = 0
|
||||
page.charOffset = 0
|
||||
page.lastSavedAbs = -1
|
||||
// sentinella "fine capitolo": va oltre la lunghezza, così
|
||||
// offsetToFrac porta all'ultima pagina
|
||||
page.pendingRestore = true
|
||||
page.restoreFrac = 1.0
|
||||
restoreTimer.start()
|
||||
var prev = page.currentChapter - 1
|
||||
page.currentChapter = prev
|
||||
reader.html = book.chapterRichText(prev)
|
||||
// posizione esplicita: fondo del capitolo precedente
|
||||
reader.goToPosition(Math.max(0, reader.characterCount - 1))
|
||||
syncState()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,6 +324,17 @@ Page {
|
||||
return Math.round(16 * page.fontScale * 100) / 100
|
||||
}
|
||||
|
||||
// aggiorna lo stato per la cover (titolo + capitolo + percentuale)
|
||||
function updateCoverState() {
|
||||
if (book.chapterCount <= 0)
|
||||
return
|
||||
var pct = Math.round(page.overallProgress() * 100)
|
||||
readerState.setReading(book.title(),
|
||||
(page.currentChapter + 1) + "/" + book.chapterCount
|
||||
+ " · " + book.chapterTitle(page.currentChapter)
|
||||
+ " · " + pct + "%")
|
||||
}
|
||||
|
||||
function showChapterList() {
|
||||
var dialog = pageStack.push(Qt.resolvedUrl("ChapterListDialog.qml"),
|
||||
{ model: book.chapterList() })
|
||||
@@ -391,14 +343,23 @@ Page {
|
||||
})
|
||||
}
|
||||
|
||||
// anteprima live dal dialogo margini: valori non ancora persistiti
|
||||
// anteprima live dal dialogo margini: la geometria dell'item cambia e il
|
||||
// motore si ri-impagina mantenendo la posizione di lettura
|
||||
function previewMargins(lr, tb) {
|
||||
page.previewMarginL = lr
|
||||
page.previewMarginT = tb
|
||||
// il reflow è immediato (binding); mantieni la posizione per caratteri
|
||||
page.restoreFrac = offsetToFrac(page.charOffset, page.currentChapter)
|
||||
page.pendingRestore = true
|
||||
restoreTimer.start()
|
||||
}
|
||||
|
||||
// apre un capitolo e va alla posizione indicata (metrica capitolo)
|
||||
function loadChapter(index, offset) {
|
||||
page.currentChapter = index
|
||||
reader.html = book.chapterRichText(index)
|
||||
var target = offset >= 0
|
||||
? Math.round((offset / Math.max(1, book.chapterTextLength(index)))
|
||||
* reader.characterCount)
|
||||
: 0
|
||||
reader.goToPosition(target)
|
||||
syncState()
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
@@ -421,14 +382,16 @@ Page {
|
||||
}
|
||||
}
|
||||
page.ready = true
|
||||
page.currentChapter = ch
|
||||
page.pendingRestore = true
|
||||
page.restoreFrac = off >= 0 ? offsetToFrac(off, ch) : 0.0
|
||||
restoreTimer.start()
|
||||
loadChapter(ch, off)
|
||||
}
|
||||
|
||||
Component.onDestruction: readerState.clearReading()
|
||||
|
||||
onStatusChanged: {
|
||||
if (status === PageStatus.Inactive) {
|
||||
if (status === PageStatus.Active) {
|
||||
if (page.ready && book.chapterCount > 0)
|
||||
updateCoverState()
|
||||
} else if (status === PageStatus.Inactive) {
|
||||
maybeSave(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import Sailfish.Silica 1.0
|
||||
Page {
|
||||
id: page
|
||||
|
||||
allowedOrientations: Orientation.Portrait
|
||||
|
||||
SilicaFlickable {
|
||||
anchors.fill: parent
|
||||
contentHeight: column.height
|
||||
|
||||
+12
-28
@@ -4,6 +4,8 @@ import Sailfish.Silica 1.0
|
||||
Page {
|
||||
id: page
|
||||
|
||||
allowedOrientations: Orientation.Portrait
|
||||
|
||||
property string testResult: ""
|
||||
property bool testOk: false
|
||||
property bool testing: false
|
||||
@@ -41,13 +43,14 @@ Page {
|
||||
onTextChanged: appSettings.username = text
|
||||
}
|
||||
|
||||
TextField {
|
||||
PasswordField {
|
||||
id: passField
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
label: qsTr("Password")
|
||||
text: appSettings.password
|
||||
echoMode: TextInput.Password
|
||||
// Icona nativa per mostrare/nascondere il testo digitato
|
||||
showEchoModeToggle: true
|
||||
onTextChanged: appSettings.password = text
|
||||
}
|
||||
|
||||
@@ -58,32 +61,6 @@ Page {
|
||||
onCheckedChanged: appSettings.ignoreSslErrors = checked
|
||||
}
|
||||
|
||||
SectionHeader { text: qsTr("Reader") }
|
||||
|
||||
Slider {
|
||||
id: marginSlider
|
||||
width: parent.width
|
||||
label: qsTr("Side margins")
|
||||
minimumValue: 0
|
||||
maximumValue: 80
|
||||
stepSize: 4
|
||||
value: appSettings.readerMargin
|
||||
valueText: value + " px"
|
||||
onValueChanged: appSettings.readerMargin = value
|
||||
}
|
||||
|
||||
Slider {
|
||||
id: marginVSlider
|
||||
width: parent.width
|
||||
label: qsTr("Top and bottom margin")
|
||||
minimumValue: 0
|
||||
maximumValue: 80
|
||||
stepSize: 4
|
||||
value: appSettings.readerMarginV
|
||||
valueText: value + " px"
|
||||
onValueChanged: appSettings.readerMarginV = value
|
||||
}
|
||||
|
||||
SectionHeader { text: qsTr("Connection") }
|
||||
|
||||
Button {
|
||||
@@ -171,11 +148,18 @@ Page {
|
||||
if (base.indexOf("http://") !== 0 && base.indexOf("https://") !== 0)
|
||||
base = "https://" + base
|
||||
appSettings.serverUrl = base
|
||||
appSettings.flush() // i campi di testo scrivono senza sync a ogni tasto
|
||||
page.testing = true
|
||||
page.testResult = qsTr("Testing…")
|
||||
apiClient.getFeed(base + "/opds")
|
||||
}
|
||||
|
||||
// uscita dalla pagina: scrivi su disco le modifiche in sospeso
|
||||
onStatusChanged: {
|
||||
if (status === PageStatus.Inactive)
|
||||
appSettings.flush()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: apiClient
|
||||
onFeedReady: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Name: harbour-calibreweb
|
||||
Summary: Client OPDS per Calibre Web
|
||||
Version: 0.4.7
|
||||
Version: 0.5.1
|
||||
Release: 1
|
||||
Group: Qt/Qt
|
||||
License: MIT
|
||||
@@ -24,6 +24,64 @@ reflow del testo, margini configurabili e posizione salvata
|
||||
in locale.
|
||||
|
||||
%changelog
|
||||
* Mon Oct 05 2026 Carlo Baratto <carlo@carlobaratto.it> - 0.5.1-1
|
||||
- Impostazioni: il campo Password e' un PasswordField con
|
||||
showEchoModeToggle, quindi l'icona nel campo mostra/nasconde il
|
||||
testo digitato (nessuna nuova stringa da tradurre)
|
||||
* Mon Sep 07 2026 Carlo Baratto <carlo@carlobaratto.it> - 0.5.0-1
|
||||
- Nuovo motore di lettura: PagedTextItem (QQuickPaintedItem + QTextDocument,
|
||||
pattern di harbour-books) impagina il capitolo in pagine misurate dal layout.
|
||||
Niente piu' Flickable a step ne' "deficit": pagine esatte, ultima pagina
|
||||
allineata, margini e inset notch validi su ogni pagina, posizione di lettura
|
||||
per carattere che sopravvive a reflow del font e rotazione (senza timer).
|
||||
Segnalibri invariati (stessa metrica, chiavi v2 compatibili)
|
||||
|
||||
* Mon Sep 07 2026 Carlo Baratto <carlo@carlobaratto.it> - 0.4.13-1
|
||||
- Lettore: il testo non finisce piu' sotto il notch del display (Jolla Phone
|
||||
2026): in portrait la finestra di lettura parte piu' in basso di un'altezza
|
||||
barra di stato (Theme.statusBarHeight) oltre al margine scelto
|
||||
|
||||
* Mon Sep 07 2026 Carlo Baratto <carlo@carlobaratto.it> - 0.4.12-1
|
||||
- Default margine verticale lettore 24 (era 0): il testo arrivava a filo dei
|
||||
bordi alto/basso su config senza valore o migrate dalle build vecchie
|
||||
(la migrazione non scrive piu' chiavi margine spurie a 0)
|
||||
- MarginDialog coerente: slider "Top and bottom" parte da 24
|
||||
|
||||
* Fri Aug 21 2026 Carlo Baratto <carlo@carlobaratto.it> - 0.4.11-1
|
||||
- Rimossi gli slider margini dalle Impostazioni: i margini si regolano solo dal
|
||||
lettore (Margini del testo nel PullDownMenu)
|
||||
- Margini alto/basso STRUTTURALI: la finestra di lettura vive dentro i margini
|
||||
(top/bottom margin del Flickable): il testo non puo' uscire dai bordi e i
|
||||
margini sono identici su ogni pagina (via le fasce colorate della 0.4.9/0.4.10)
|
||||
|
||||
* Fri Aug 21 2026 Carlo Baratto <carlo@carlobaratto.it> - 0.4.10-1
|
||||
- Fix margini lettore (0.4.9): sfondo di lettura UNIFORME nel lettore (colore
|
||||
pieno per tema, niente gradiente di sistema). Le fasce dei margini usano lo
|
||||
stesso identico colore dello sfondo: margine visibile su ogni pagina, senza
|
||||
bande stonate e senza effetto "fascia che copre il testo"
|
||||
|
||||
* Fri Aug 21 2026 Carlo Baratto <carlo@carlobaratto.it> - 0.4.9-1
|
||||
- Margini verticali su OGNI schermata del lettore (non solo a inizio capitolo):
|
||||
due fasce opache (colore di sfondo) sopra e sotto la viewport coprono il testo
|
||||
a cavallo dei bordi; il padding inferiore del contenitore include il deficit
|
||||
dell'ultima pagina, cosi' max-scroll e' un multiplo esatto del passo e
|
||||
l'ultima pagina non ripete righe della penultima. Anteprima live dal dialog
|
||||
margini inclusa
|
||||
|
||||
* Fri Aug 21 2026 Carlo Baratto <carlo@carlobaratto.it> - 0.4.8-1
|
||||
- Best practice audit: orientazioni per pagina (finestra senza vincoli, il
|
||||
lettore ora ruota davvero in landscape; pagine di lista in portrait;
|
||||
rimossa la proprieta' privata _defaultPageOrientations)
|
||||
- Licenza MIT installata nel pacchetto (/usr/share/licenses/harbour-calibreweb)
|
||||
- EPUB: lunghezze testo LAZY per capitolo (l'apertura non parse piu' tutto il
|
||||
libro: niente freeze sui libri grossi; cache al primo accesso)
|
||||
- Cover dinamica: con un libro aperto mostra titolo e posizione di lettura
|
||||
(ReaderState esposto come context property, aggiornato dal ReaderPage)
|
||||
- Impostazioni: niente sync su disco a ogni tasto nei campi di testo
|
||||
(flush() all'uscita della pagina e prima del test connessione)
|
||||
- Pulizia: rimossa proprieta' morta chapterFraction, commenti obsoleti
|
||||
aggiornati (settings.h, .pro)
|
||||
|
||||
* Fri Aug 21 2026 Carlo Baratto <carlo@carlobaratto.it> - 0.4.7-1
|
||||
- Fix margini lettore: il passo di pagina era la viewport intera, quindi il
|
||||
testo scorreva a filo dei bordi e i margini verticali si vedevano solo
|
||||
@@ -199,6 +257,19 @@ desktop-file-validate %{buildroot}%{_datadir}/applications/%{name}.desktop || tr
|
||||
mkdir -p %{buildroot}%{_datadir}/%{name}/translations
|
||||
install -m 0644 %{_builddir}/%{name}_it.qm %{buildroot}%{_datadir}/%{name}/translations/
|
||||
|
||||
# MIT license: installed in the package (Harbour best practice).
|
||||
mkdir -p %{buildroot}%{_datadir}/licenses/%{name}
|
||||
if [ -f %{_builddir}/../%{name}-%{version}/LICENSE ]; then
|
||||
install -m 0644 %{_builddir}/../%{name}-%{version}/LICENSE %{buildroot}%{_datadir}/licenses/%{name}/
|
||||
elif [ -f %{_builddir}/%{name}-%{version}/LICENSE ]; then
|
||||
install -m 0644 %{_builddir}/%{name}-%{version}/LICENSE %{buildroot}%{_datadir}/licenses/%{name}/
|
||||
elif [ -f %{_builddir}/LICENSE ]; then
|
||||
install -m 0644 %{_builddir}/LICENSE %{buildroot}%{_datadir}/licenses/%{name}/
|
||||
else
|
||||
echo "ERROR: LICENSE not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
%files
|
||||
%defattr(-,root,root,-)
|
||||
%{_bindir}/%{name}
|
||||
@@ -209,3 +280,4 @@ install -m 0644 %{_builddir}/%{name}_it.qm %{buildroot}%{_datadir}/%{name}/trans
|
||||
%{_datadir}/icons/hicolor/172x172/apps/%{name}.png
|
||||
%{_datadir}/%{name}/qml
|
||||
%{_datadir}/%{name}/translations/%{name}*.qm
|
||||
%{_datadir}/licenses/%{name}/LICENSE
|
||||
|
||||
+20
-10
@@ -490,8 +490,14 @@ bool EpubBook::open(const QString &epubPath)
|
||||
if (!parseOpf())
|
||||
return false;
|
||||
const bool ok = !m_chapters.isEmpty();
|
||||
if (ok)
|
||||
computeTextLengths();
|
||||
if (ok) {
|
||||
// lunghezze testo LAZY: -1 = da calcolare al primo accesso. L'apertura
|
||||
// resta veloce anche per libri grossi (niente parse di tutti i capitoli).
|
||||
m_textLengths.clear();
|
||||
m_textLengths.reserve(m_chapters.size());
|
||||
for (int i = 0; i < m_chapters.size(); ++i)
|
||||
m_textLengths.append(-1);
|
||||
}
|
||||
emit changed();
|
||||
return ok;
|
||||
}
|
||||
@@ -763,24 +769,26 @@ QString EpubBook::chapterRichText(int index) const
|
||||
return htmlToRichText(html);
|
||||
}
|
||||
|
||||
void EpubBook::computeTextLengths()
|
||||
// calcola (e cache) la lunghezza del capitolo index se non ancora nota
|
||||
void EpubBook::ensureTextLength(int index) const
|
||||
{
|
||||
m_textLengths.clear();
|
||||
for (const EpubChapter &c : m_chapters) {
|
||||
if (index < 0 || index >= m_textLengths.size() || m_textLengths.at(index) >= 0)
|
||||
return;
|
||||
const EpubChapter &c = m_chapters.at(index);
|
||||
QFile f(c.path);
|
||||
if (!f.open(QIODevice::ReadOnly)) {
|
||||
m_textLengths.append(0);
|
||||
continue;
|
||||
m_textLengths[index] = 0;
|
||||
return;
|
||||
}
|
||||
const QString html = QString::fromUtf8(f.readAll());
|
||||
m_textLengths.append(plainTextLength(html));
|
||||
}
|
||||
m_textLengths[index] = plainTextLength(html);
|
||||
}
|
||||
|
||||
int EpubBook::chapterTextLength(int index) const
|
||||
{
|
||||
if (index < 0 || index >= m_textLengths.size())
|
||||
return 0;
|
||||
ensureTextLength(index);
|
||||
return m_textLengths.at(index);
|
||||
}
|
||||
|
||||
@@ -788,8 +796,10 @@ qint64 EpubBook::chapterStartChar(int index) const
|
||||
{
|
||||
qint64 start = 0;
|
||||
const int end = qMin(index, m_textLengths.size());
|
||||
for (int i = 0; i < end; ++i)
|
||||
for (int i = 0; i < end; ++i) {
|
||||
ensureTextLength(i);
|
||||
start += m_textLengths.at(i);
|
||||
}
|
||||
return start;
|
||||
}
|
||||
|
||||
|
||||
+5
-3
@@ -66,15 +66,17 @@ private:
|
||||
void parseTocEpub3(const QString &navPath);
|
||||
void parseTocEpub2(const QString &ncxPath);
|
||||
QString absolutePath(const QString &relativeToOpfDir) const;
|
||||
// lunghezze in caratteri del testo visibile, calcolate all'open
|
||||
void computeTextLengths();
|
||||
// calcola (e cache) la lunghezza in caratteri del capitolo al primo accesso
|
||||
void ensureTextLength(int index) const;
|
||||
|
||||
QString m_baseDir;
|
||||
QString m_opfDir;
|
||||
QString m_title;
|
||||
QString m_author;
|
||||
QList<EpubChapter> m_chapters;
|
||||
QList<int> m_textLengths;
|
||||
// lunghezze testo per capitolo, calcolate LAZY al primo accesso
|
||||
// (-1 = non ancora calcolata); mutable per la cache da metodi const
|
||||
mutable QList<int> m_textLengths;
|
||||
};
|
||||
|
||||
#endif // EPUB_H
|
||||
|
||||
+8
-2
@@ -10,6 +10,8 @@
|
||||
#include "apiclient.h"
|
||||
#include "downloader.h"
|
||||
#include "epub.h"
|
||||
#include "pagedtextitem.h"
|
||||
#include "readerstate.h"
|
||||
#include "settings.h"
|
||||
|
||||
Q_DECL_EXPORT int main(int argc, char *argv[])
|
||||
@@ -22,7 +24,7 @@ Q_DECL_EXPORT int main(int argc, char *argv[])
|
||||
app->setOrganizationName(QStringLiteral("harbour"));
|
||||
app->setApplicationName(QStringLiteral("calibreweb"));
|
||||
// versione mostrata nelle impostazioni (tenere allineata allo spec)
|
||||
app->setApplicationVersion(QStringLiteral("0.4.7"));
|
||||
app->setApplicationVersion(QStringLiteral("0.5.1"));
|
||||
|
||||
Settings settings;
|
||||
|
||||
@@ -56,16 +58,20 @@ Q_DECL_EXPORT int main(int argc, char *argv[])
|
||||
|
||||
QQuickView *view = SailfishApp::createView();
|
||||
|
||||
qDebug() << "harbour-calibreweb v0.4.7 build" << __DATE__ << __TIME__;
|
||||
qDebug() << "harbour-calibreweb v0.5.1 build" << __DATE__ << __TIME__;
|
||||
|
||||
qmlRegisterType<EpubBook>("harbour.calibreweb", 1, 0, "EpubBook");
|
||||
qmlRegisterType<PagedTextItem>("harbour.calibreweb", 1, 0, "PagedTextItem");
|
||||
|
||||
ApiClient apiClient(&settings);
|
||||
Downloader downloader(&settings);
|
||||
// stato di lettura corrente (per la cover quando l'app va in background)
|
||||
ReaderState readerState(&settings);
|
||||
|
||||
view->rootContext()->setContextProperty(QStringLiteral("appSettings"), &settings);
|
||||
view->rootContext()->setContextProperty(QStringLiteral("apiClient"), &apiClient);
|
||||
view->rootContext()->setContextProperty(QStringLiteral("downloader"), &downloader);
|
||||
view->rootContext()->setContextProperty(QStringLiteral("readerState"), &readerState);
|
||||
view->rootContext()->setContextProperty(QStringLiteral("appVersion"),
|
||||
QCoreApplication::applicationVersion());
|
||||
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
#include "pagedtextitem.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <QAbstractTextDocumentLayout>
|
||||
#include <QTextBlock>
|
||||
#include <QTextLayout>
|
||||
#include <QFont>
|
||||
#include <QPointF>
|
||||
#include <QRectF>
|
||||
|
||||
PagedTextItem::PagedTextItem(QQuickItem *parent)
|
||||
: QQuickPaintedItem(parent)
|
||||
, m_fontPixelSize(16)
|
||||
, m_page(0)
|
||||
, m_pendingPosition(-1)
|
||||
, m_layoutPageHeight(1)
|
||||
{
|
||||
// nessun margine interno: i margini li impone la geometria dell'item in QML
|
||||
m_doc.setDocumentMargin(0);
|
||||
applyFont();
|
||||
}
|
||||
|
||||
void PagedTextItem::applyFont()
|
||||
{
|
||||
QFont f = m_doc.defaultFont();
|
||||
if (!m_fontFamily.isEmpty())
|
||||
f.setFamily(m_fontFamily);
|
||||
f.setPixelSize(qMax(4, m_fontPixelSize));
|
||||
m_doc.setDefaultFont(f);
|
||||
}
|
||||
|
||||
void PagedTextItem::setHtml(const QString &html)
|
||||
{
|
||||
if (m_html == html)
|
||||
return;
|
||||
m_html = html;
|
||||
m_page = 0;
|
||||
m_doc.setHtml(m_html);
|
||||
if (!m_baseUrl.isEmpty())
|
||||
m_doc.setBaseUrl(QUrl(m_baseUrl));
|
||||
relayout(false);
|
||||
emit htmlChanged();
|
||||
}
|
||||
|
||||
void PagedTextItem::setBaseUrl(const QString &url)
|
||||
{
|
||||
if (m_baseUrl == url)
|
||||
return;
|
||||
m_baseUrl = url;
|
||||
m_doc.setBaseUrl(QUrl(url));
|
||||
emit baseUrlChanged();
|
||||
update();
|
||||
}
|
||||
|
||||
void PagedTextItem::setFontFamily(const QString &family)
|
||||
{
|
||||
if (m_fontFamily == family)
|
||||
return;
|
||||
// posizione letta PRIMA di toccare il font: il layout e' ancora coerente
|
||||
const int keep = positionAtPage(m_page);
|
||||
m_fontFamily = family;
|
||||
applyFont();
|
||||
relayoutKeeping(keep);
|
||||
emit fontFamilyChanged();
|
||||
}
|
||||
|
||||
void PagedTextItem::setFontPixelSize(int px)
|
||||
{
|
||||
px = qMax(4, px);
|
||||
if (m_fontPixelSize == px)
|
||||
return;
|
||||
// posizione letta PRIMA del reflow (vedi setFontFamily)
|
||||
const int keep = positionAtPage(m_page);
|
||||
m_fontPixelSize = px;
|
||||
applyFont();
|
||||
// reflow: mantiene la posizione di lettura per carattere
|
||||
relayoutKeeping(keep);
|
||||
emit fontPixelSizeChanged();
|
||||
}
|
||||
|
||||
void PagedTextItem::setTextColor(const QColor &color)
|
||||
{
|
||||
if (m_textColor == color)
|
||||
return;
|
||||
m_textColor = color;
|
||||
emit textColorChanged();
|
||||
update();
|
||||
}
|
||||
|
||||
void PagedTextItem::setPage(int page)
|
||||
{
|
||||
const int last = pageCount() - 1;
|
||||
page = qBound(0, page, qMax(0, last));
|
||||
if (m_page == page)
|
||||
return;
|
||||
m_page = page;
|
||||
emit pageChanged();
|
||||
emit positionChanged();
|
||||
update();
|
||||
}
|
||||
|
||||
void PagedTextItem::nextPage()
|
||||
{
|
||||
setPage(m_page + 1);
|
||||
}
|
||||
|
||||
void PagedTextItem::previousPage()
|
||||
{
|
||||
setPage(m_page - 1);
|
||||
}
|
||||
|
||||
void PagedTextItem::goToPosition(int pos)
|
||||
{
|
||||
if (width() <= 0 || height() <= 0) {
|
||||
// geometria non ancora risolta: applica al primo layout
|
||||
m_pendingPosition = pos;
|
||||
return;
|
||||
}
|
||||
setPage(pageForPosition(pos));
|
||||
}
|
||||
|
||||
int PagedTextItem::pageCount() const
|
||||
{
|
||||
const int n = m_doc.pageCount();
|
||||
return n > 0 ? n : 1;
|
||||
}
|
||||
|
||||
int PagedTextItem::characterCount() const
|
||||
{
|
||||
return m_doc.characterCount();
|
||||
}
|
||||
|
||||
int PagedTextItem::position() const
|
||||
{
|
||||
return positionAtPage(m_page);
|
||||
}
|
||||
|
||||
qreal PagedTextItem::pageHeight() const
|
||||
{
|
||||
// altezza dell'ULTIMO layout: se la geometria e' appena cambiata ma il
|
||||
// documento non e' ancora stato ri-impaginato, le letture restano coerenti
|
||||
return m_layoutPageHeight > 0 ? m_layoutPageHeight : qMax(qreal(1), height());
|
||||
}
|
||||
|
||||
int PagedTextItem::positionAtPage(int page) const
|
||||
{
|
||||
if (m_doc.isEmpty() || width() <= 0 || height() <= 0)
|
||||
return 0;
|
||||
QAbstractTextDocumentLayout *layout = m_doc.documentLayout();
|
||||
if (!layout)
|
||||
return 0;
|
||||
const qreal ph = pageHeight();
|
||||
// punto appena dentro la pagina: hitTest dà il carattere piu' vicino
|
||||
const QPointF probe(qMin(qreal(4), width() / 2), page * ph + 1);
|
||||
const int pos = layout->hitTest(probe, Qt::FuzzyHit);
|
||||
if (pos < 0)
|
||||
return 0;
|
||||
return qBound(0, pos, qMax(0, m_doc.characterCount() - 1));
|
||||
}
|
||||
|
||||
int PagedTextItem::pageForPosition(int pos) const
|
||||
{
|
||||
if (m_doc.isEmpty() || width() <= 0 || height() <= 0)
|
||||
return 0;
|
||||
const int len = m_doc.characterCount();
|
||||
const int at = qBound(0, pos, qMax(0, len - 1));
|
||||
// findBlock invece di QTextCursor: metodo const (niente cast sul documento)
|
||||
const QTextBlock block = m_doc.findBlock(at);
|
||||
QRectF rect;
|
||||
if (QAbstractTextDocumentLayout *layout = m_doc.documentLayout())
|
||||
rect = layout->blockBoundingRect(block);
|
||||
qreal y = rect.top();
|
||||
if (block.layout()) {
|
||||
// y della riga che contiene il carattere, relativa al blocco: cosi'
|
||||
// anche i paragrafi lunghi (spezzati su piu' pagine) sono precisi
|
||||
const int inBlock = qBound(0, at - block.position(),
|
||||
qMax(0, block.length() - 1));
|
||||
const QTextLine line = block.layout()->lineForTextPosition(inBlock);
|
||||
if (line.isValid())
|
||||
y += line.y();
|
||||
}
|
||||
return qMax(0, int(y / pageHeight()));
|
||||
}
|
||||
|
||||
void PagedTextItem::clampPage()
|
||||
{
|
||||
const int last = pageCount() - 1;
|
||||
m_page = qBound(0, m_page, qMax(0, last));
|
||||
}
|
||||
|
||||
void PagedTextItem::relayoutKeeping(int pos)
|
||||
{
|
||||
if (width() <= 0 || height() <= 0)
|
||||
return;
|
||||
m_doc.setPageSize(QSizeF(width(), height()));
|
||||
m_layoutPageHeight = qMax(qreal(1), height());
|
||||
if (m_pendingPosition >= 0) {
|
||||
// posizione chiesta prima del layout (ripristino all'apertura)
|
||||
m_page = pageForPosition(m_pendingPosition);
|
||||
m_pendingPosition = -1;
|
||||
} else if (pos >= 0) {
|
||||
m_page = pageForPosition(pos);
|
||||
}
|
||||
clampPage();
|
||||
emit layoutChanged();
|
||||
emit positionChanged();
|
||||
update();
|
||||
}
|
||||
|
||||
void PagedTextItem::relayout(bool keepPosition)
|
||||
{
|
||||
relayoutKeeping(keepPosition ? positionAtPage(m_page) : -1);
|
||||
}
|
||||
|
||||
void PagedTextItem::geometryChanged(const QRectF &newGeometry,
|
||||
const QRectF &oldGeometry)
|
||||
{
|
||||
QQuickPaintedItem::geometryChanged(newGeometry, oldGeometry);
|
||||
if (newGeometry.size() != oldGeometry.size())
|
||||
relayout(true);
|
||||
}
|
||||
|
||||
void PagedTextItem::renderPage(QPainter *painter) const
|
||||
{
|
||||
if (!painter || width() <= 0 || height() <= 0 || m_doc.isEmpty())
|
||||
return;
|
||||
const qreal ph = pageHeight();
|
||||
painter->save();
|
||||
painter->setRenderHint(QPainter::TextAntialiasing, true);
|
||||
painter->setRenderHint(QPainter::Antialiasing, true);
|
||||
painter->setClipRect(QRectF(0, 0, width(), ph));
|
||||
painter->translate(0, -m_page * ph);
|
||||
|
||||
QAbstractTextDocumentLayout::PaintContext ctx;
|
||||
// il clip del contesto e' in coordinate documento
|
||||
ctx.clip = QRectF(0, m_page * ph, width(), ph);
|
||||
if (m_textColor.isValid())
|
||||
ctx.palette.setColor(QPalette::Text, m_textColor);
|
||||
m_doc.documentLayout()->draw(painter, ctx);
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
void PagedTextItem::paint(QPainter *painter)
|
||||
{
|
||||
renderPage(painter);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
#ifndef PAGEDTEXTITEM_H
|
||||
#define PAGEDTEXTITEM_H
|
||||
|
||||
// Motore di paginazione del lettore: pagina il rich text del capitolo in
|
||||
// pagine ESATTE usando QTextDocument, e lo disegna con QPainter dentro un item
|
||||
// QML (pattern QQuickPaintedItem, come harbour-books di Slava Monich).
|
||||
//
|
||||
// Perche': QTextDocument con pageSize imposta il "page breaking" del layout,
|
||||
// quindi le pagine sono misurate dal layout stesso (niente stime, niente
|
||||
// scroll, niente testo che sborda) e hitTest mappa pagina <-> carattere, che
|
||||
// serve ai segnalibri e al ripristino della posizione dopo un reflow.
|
||||
#include <QQuickPaintedItem>
|
||||
#include <QTextDocument>
|
||||
#include <QColor>
|
||||
#include <QUrl>
|
||||
|
||||
class PagedTextItem : public QQuickPaintedItem
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QString html READ html WRITE setHtml NOTIFY htmlChanged)
|
||||
Q_PROPERTY(QString baseUrl READ baseUrl WRITE setBaseUrl NOTIFY baseUrlChanged)
|
||||
Q_PROPERTY(QString fontFamily READ fontFamily WRITE setFontFamily
|
||||
NOTIFY fontFamilyChanged)
|
||||
Q_PROPERTY(int fontPixelSize READ fontPixelSize WRITE setFontPixelSize
|
||||
NOTIFY fontPixelSizeChanged)
|
||||
Q_PROPERTY(QColor textColor READ textColor WRITE setTextColor
|
||||
NOTIFY textColorChanged)
|
||||
// pagina corrente (0-based) e numero di pagine del capitolo caricato
|
||||
Q_PROPERTY(int page READ page WRITE setPage NOTIFY pageChanged)
|
||||
Q_PROPERTY(int pageCount READ pageCount NOTIFY layoutChanged)
|
||||
// primo carattere visibile nella pagina corrente (per i segnalibri)
|
||||
Q_PROPERTY(int position READ position NOTIFY positionChanged)
|
||||
Q_PROPERTY(int characterCount READ characterCount NOTIFY layoutChanged)
|
||||
|
||||
public:
|
||||
explicit PagedTextItem(QQuickItem *parent = nullptr);
|
||||
|
||||
void paint(QPainter *painter) override;
|
||||
// disegno della pagina corrente; pubblico per i test headless
|
||||
void renderPage(QPainter *painter) const;
|
||||
|
||||
QString html() const { return m_html; }
|
||||
void setHtml(const QString &html);
|
||||
QString baseUrl() const { return m_baseUrl; }
|
||||
void setBaseUrl(const QString &url);
|
||||
QString fontFamily() const { return m_fontFamily; }
|
||||
void setFontFamily(const QString &family);
|
||||
int fontPixelSize() const { return m_fontPixelSize; }
|
||||
void setFontPixelSize(int px);
|
||||
QColor textColor() const { return m_textColor; }
|
||||
void setTextColor(const QColor &color);
|
||||
|
||||
int page() const { return m_page; }
|
||||
void setPage(int page);
|
||||
int pageCount() const;
|
||||
int position() const;
|
||||
int characterCount() const;
|
||||
|
||||
Q_INVOKABLE void nextPage();
|
||||
Q_INVOKABLE void previousPage();
|
||||
// sposta la vista sul carattere indicato (ripristino posizione/reflow)
|
||||
Q_INVOKABLE void goToPosition(int pos);
|
||||
Q_INVOKABLE int positionAtPage(int page) const;
|
||||
Q_INVOKABLE int pageForPosition(int pos) const;
|
||||
|
||||
signals:
|
||||
void htmlChanged();
|
||||
void baseUrlChanged();
|
||||
void fontFamilyChanged();
|
||||
void fontPixelSizeChanged();
|
||||
void textColorChanged();
|
||||
void pageChanged();
|
||||
void positionChanged();
|
||||
// il documento e' stato ri-impaginato (resize, font, nuovo capitolo)
|
||||
void layoutChanged();
|
||||
|
||||
protected:
|
||||
void geometryChanged(const QRectF &newGeometry,
|
||||
const QRectF &oldGeometry) override;
|
||||
|
||||
private:
|
||||
void applyFont();
|
||||
// ri-impagina e (se pos >= 0) riporta la vista sul carattere indicato
|
||||
void relayoutKeeping(int pos);
|
||||
void relayout(bool keepPosition);
|
||||
void clampPage();
|
||||
qreal pageHeight() const;
|
||||
|
||||
QTextDocument m_doc;
|
||||
QString m_html;
|
||||
QString m_baseUrl;
|
||||
QString m_fontFamily;
|
||||
int m_fontPixelSize;
|
||||
QColor m_textColor;
|
||||
int m_page;
|
||||
// posizione richiesta prima che l'item avesse una geometria valida:
|
||||
// applicata al primo layout (senza, il ripristino del segnalibro
|
||||
// atterrerebbe sempre a inizio capitolo)
|
||||
int m_pendingPosition;
|
||||
// altezza pagina dell'ultimo layout: le letture di posizione restano
|
||||
// coerenti anche se la geometria dell'item e' appena cambiata
|
||||
qreal m_layoutPageHeight;
|
||||
};
|
||||
|
||||
#endif // PAGEDTEXTITEM_H
|
||||
@@ -0,0 +1,27 @@
|
||||
#include "readerstate.h"
|
||||
|
||||
ReaderState::ReaderState(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_active(false)
|
||||
{
|
||||
}
|
||||
|
||||
void ReaderState::setReading(const QString &title, const QString &subtitle)
|
||||
{
|
||||
if (m_active && m_title == title && m_subtitle == subtitle)
|
||||
return;
|
||||
m_active = true;
|
||||
m_title = title;
|
||||
m_subtitle = subtitle;
|
||||
emit changed();
|
||||
}
|
||||
|
||||
void ReaderState::clearReading()
|
||||
{
|
||||
if (!m_active)
|
||||
return;
|
||||
m_active = false;
|
||||
m_title.clear();
|
||||
m_subtitle.clear();
|
||||
emit changed();
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
#ifndef READERSTATE_H
|
||||
#define READERSTATE_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
// Stato di lettura corrente, esposto come context property "readerState":
|
||||
// la cover mostra il libro aperto (titolo + posizione) invece dell'icona fissa.
|
||||
class ReaderState : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(bool active READ active NOTIFY changed)
|
||||
Q_PROPERTY(QString title READ title NOTIFY changed)
|
||||
Q_PROPERTY(QString subtitle READ subtitle NOTIFY changed)
|
||||
|
||||
public:
|
||||
explicit ReaderState(QObject *parent = 0);
|
||||
|
||||
bool active() const { return m_active; }
|
||||
QString title() const { return m_title; }
|
||||
QString subtitle() const { return m_subtitle; }
|
||||
|
||||
// aggiorna lo stato quando il lettore mostra un libro (chiamata dal QML)
|
||||
Q_INVOKABLE void setReading(const QString &title, const QString &subtitle);
|
||||
// il libro è stato chiuso: la cover torna all'icona predefinita
|
||||
Q_INVOKABLE void clearReading();
|
||||
|
||||
signals:
|
||||
void changed();
|
||||
|
||||
private:
|
||||
bool m_active;
|
||||
QString m_title;
|
||||
QString m_subtitle;
|
||||
};
|
||||
|
||||
#endif // READERSTATE_H
|
||||
+16
-8
@@ -55,10 +55,14 @@ void Settings::migrateIfNeeded()
|
||||
m_settings.setValue(QStringLiteral("password"), src.value(QStringLiteral("password")));
|
||||
m_settings.setValue(QStringLiteral("ignoreSslErrors"),
|
||||
src.value(QStringLiteral("ignoreSslErrors")));
|
||||
// margini copiati SOLO se espliciti nel sorgente: se mancano, niente
|
||||
// chiave nel nuovo file -> vale il default del getter (24)
|
||||
if (src.contains(QStringLiteral("readerMargin")))
|
||||
m_settings.setValue(QStringLiteral("readerMargin"),
|
||||
src.value(QStringLiteral("readerMargin"), 24));
|
||||
src.value(QStringLiteral("readerMargin")));
|
||||
if (src.contains(QStringLiteral("readerMarginV")))
|
||||
m_settings.setValue(QStringLiteral("readerMarginV"),
|
||||
src.value(QStringLiteral("readerMarginV"), 0));
|
||||
src.value(QStringLiteral("readerMarginV")));
|
||||
// segnalibri di lettura per libro+formato
|
||||
src.beginGroup(QStringLiteral("bookmarks"));
|
||||
const QStringList groups = src.childGroups();
|
||||
@@ -115,9 +119,9 @@ void Settings::setServerUrl(const QString &value)
|
||||
{
|
||||
if (value == serverUrl())
|
||||
return;
|
||||
// niente sync a ogni tasto (flash wear): il flush avviene all'uscita
|
||||
// dalle impostazioni o prima delle richieste di rete (vedi flush())
|
||||
m_settings.setValue(QStringLiteral("serverUrl"), value);
|
||||
m_settings.sync(); // scrivi subito su disco: l'app può crashare alla chiusura (WebView)
|
||||
openPermissions();
|
||||
emit serverUrlChanged();
|
||||
emit baseUrlChanged();
|
||||
}
|
||||
@@ -132,8 +136,6 @@ void Settings::setUsername(const QString &value)
|
||||
if (value == username())
|
||||
return;
|
||||
m_settings.setValue(QStringLiteral("username"), value);
|
||||
m_settings.sync();
|
||||
openPermissions();
|
||||
emit usernameChanged();
|
||||
}
|
||||
|
||||
@@ -147,9 +149,13 @@ void Settings::setPassword(const QString &value)
|
||||
if (value == password())
|
||||
return;
|
||||
m_settings.setValue(QStringLiteral("password"), value);
|
||||
emit passwordChanged();
|
||||
}
|
||||
|
||||
void Settings::flush()
|
||||
{
|
||||
m_settings.sync();
|
||||
openPermissions();
|
||||
emit passwordChanged();
|
||||
}
|
||||
|
||||
bool Settings::ignoreSslErrors() const
|
||||
@@ -184,7 +190,9 @@ void Settings::setReaderMargin(int value)
|
||||
|
||||
int Settings::readerMarginV() const
|
||||
{
|
||||
return m_settings.value(QStringLiteral("readerMarginV"), 0).toInt();
|
||||
// default 24 come il laterale: margini alto/basso visibili anche senza
|
||||
// configurazione (le build pre-0.4.12 usavano 0 come default)
|
||||
return m_settings.value(QStringLiteral("readerMarginV"), 24).toInt();
|
||||
}
|
||||
|
||||
void Settings::setReaderMarginV(int value)
|
||||
|
||||
+14
-9
@@ -55,6 +55,10 @@ public:
|
||||
|
||||
QString downloadDir() const;
|
||||
|
||||
// scrive subito su disco le modifiche in sospeso (i setter di testo non
|
||||
// fanno sync a ogni tasto: chiamare questo all'uscita dalle impostazioni)
|
||||
Q_INVOKABLE void flush();
|
||||
|
||||
// Header "Authorization: Basic ..." pronto da usare, vuoto se utente non impostato
|
||||
QString authHeader() const;
|
||||
|
||||
@@ -80,17 +84,18 @@ signals:
|
||||
void baseUrlChanged();
|
||||
|
||||
private:
|
||||
// Path fisso condiviso: l'app deve leggere/scrivere SEMPRE lo stesso file,
|
||||
// sia avviata dal launcher (utente nemo) sia da SSH di debug (defaultuser).
|
||||
// Con QSettings("harbour","calibreweb") il file dipende dalla HOME
|
||||
// dell'utente che avvia -> configurazione invisibile dall'altro utente.
|
||||
// Path canonico per la sandbox SailJail (Sailfish 4.4+/5.x): con org/app
|
||||
// impostati (harbour/calibreweb, vedi main.cpp e X-Sailjail nel desktop
|
||||
// file), AppConfigLocation = ~/.config/harbour/calibreweb, l'unica zona
|
||||
// config PERSISTENTE della sandbox (senza profilo la sandbox la nasconde).
|
||||
static QString configPath();
|
||||
// Importa i valori dalla vecchia config per-utente (~/.config/harbour/...) se
|
||||
// il file condiviso e' vuoto: copre la configurazione fatta prima del fix.
|
||||
// Importa i valori dal vecchio file per-utente (~/.config/harbour/...) se
|
||||
// il file canonico e' vuoto: copre la configurazione fatta prima del fix.
|
||||
void migrateIfNeeded();
|
||||
// Apre i permessi della catena /home/nemo/.config/harbour e del file:
|
||||
// il launcher gira come nemo (home 700), il debug SSH come defaultuser —
|
||||
// entrambi devono poter leggere/scrivere lo stesso file di configurazione.
|
||||
// Apre i permessi della catena di directory del file di configurazione:
|
||||
// l'avvio dal launcher e quello da SSH (defaultuser) devono poter
|
||||
// leggere/scrivere lo stesso file. Efficace quando l'app gira come
|
||||
// owner della home (dal launcher).
|
||||
void openPermissions();
|
||||
|
||||
QSettings m_settings;
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE TS>
|
||||
<TS version="2.1" language="it_IT">
|
||||
<context>
|
||||
<name>BookDetailPage</name>
|
||||
<message>
|
||||
<location filename="../qml/pages/BookDetailPage.qml" line="36" />
|
||||
<location filename="../qml/pages/BookDetailPage.qml" line="38"/>
|
||||
<source>Read (EPUB)</source>
|
||||
<translation>Leggi (EPUB)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/BookDetailPage.qml" line="60" />
|
||||
<location filename="../qml/pages/BookDetailPage.qml" line="62"/>
|
||||
<source>Formats</source>
|
||||
<translation>Formati</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/BookDetailPage.qml" line="112" />
|
||||
<location filename="../qml/pages/BookDetailPage.qml" line="114"/>
|
||||
<source>Downloading…</source>
|
||||
<translation>Download in corso…</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/BookDetailPage.qml" line="261" />
|
||||
<location filename="../qml/pages/BookDetailPage.qml" line="263"/>
|
||||
<source>Saved to </source>
|
||||
<translation>Salvato in </translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/BookDetailPage.qml" line="266" />
|
||||
<location filename="../qml/pages/BookDetailPage.qml" line="268"/>
|
||||
<source>Error: </source>
|
||||
<translation>Errore: </translation>
|
||||
</message>
|
||||
@@ -44,7 +45,7 @@
|
||||
<context>
|
||||
<name>CoverPage</name>
|
||||
<message>
|
||||
<location filename="../qml/cover/CoverPage.qml" line="18" />
|
||||
<location filename="../qml/cover/CoverPage.qml" line="22"/>
|
||||
<source>Calibre Web</source>
|
||||
<translation>Calibre Web</translation>
|
||||
</message>
|
||||
@@ -52,18 +53,18 @@
|
||||
<context>
|
||||
<name>FeedPage</name>
|
||||
<message>
|
||||
<location filename="../qml/pages/FeedPage.qml" line="91" />
|
||||
<location filename="../qml/pages/FeedPage.qml" line="93"/>
|
||||
<source>Refresh</source>
|
||||
<translation>Aggiorna</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/FeedPage.qml" line="95" />
|
||||
<location filename="../qml/pages/FeedPage.qml" line="97"/>
|
||||
<source>Settings</source>
|
||||
<translation>Impostazioni</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/FeedPage.qml" line="102" />
|
||||
<location filename="../qml/pages/FeedPage.qml" line="171" />
|
||||
<location filename="../qml/pages/FeedPage.qml" line="104"/>
|
||||
<location filename="../qml/pages/FeedPage.qml" line="173"/>
|
||||
<source>No results</source>
|
||||
<translation>Nessun risultato</translation>
|
||||
</message>
|
||||
@@ -71,37 +72,37 @@
|
||||
<context>
|
||||
<name>MainPage</name>
|
||||
<message>
|
||||
<location filename="../qml/pages/MainPage.qml" line="13" />
|
||||
<location filename="../qml/pages/MainPage.qml" line="15"/>
|
||||
<source>Calibre Web</source>
|
||||
<translation>Calibre Web</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/MainPage.qml" line="43" />
|
||||
<location filename="../qml/pages/MainPage.qml" line="45"/>
|
||||
<source>Search</source>
|
||||
<translation>Cerca</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/MainPage.qml" line="47" />
|
||||
<location filename="../qml/pages/MainPage.qml" line="49"/>
|
||||
<source>Settings</source>
|
||||
<translation>Impostazioni</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/MainPage.qml" line="51" />
|
||||
<location filename="../qml/pages/MainPage.qml" line="53"/>
|
||||
<source>Refresh</source>
|
||||
<translation>Aggiorna</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/MainPage.qml" line="60" />
|
||||
<location filename="../qml/pages/MainPage.qml" line="62"/>
|
||||
<source>Open the settings and enter your Calibre Web address</source>
|
||||
<translation>Apri le impostazioni e inserisci l'indirizzo del tuo Calibre Web</translation>
|
||||
<translation>Apri le impostazioni e inserisci l'indirizzo del tuo Calibre Web</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/MainPage.qml" line="77" />
|
||||
<location filename="../qml/pages/MainPage.qml" line="79"/>
|
||||
<source>Server not configured</source>
|
||||
<translation>Server non configurato</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/MainPage.qml" line="100" />
|
||||
<location filename="../qml/pages/MainPage.qml" line="102"/>
|
||||
<source>No sections found in the OPDS feed</source>
|
||||
<translation>Nessuna sezione trovata nel feed OPDS</translation>
|
||||
</message>
|
||||
@@ -137,37 +138,27 @@
|
||||
<context>
|
||||
<name>ReaderPage</name>
|
||||
<message>
|
||||
<location filename="../qml/pages/ReaderPage.qml" line="35" />
|
||||
<location filename="../qml/pages/ReaderPage.qml" line="68"/>
|
||||
<source>Table of contents</source>
|
||||
<translation>Indice capitoli</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/ReaderPage.qml" line="39" />
|
||||
<location filename="../qml/pages/ReaderPage.qml" line="72"/>
|
||||
<source>Text margins</source>
|
||||
<translation>Margini del testo</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/ReaderPage.qml" line="53" />
|
||||
<location filename="../qml/pages/ReaderPage.qml" line="87"/>
|
||||
<source>Settings</source>
|
||||
<translation>Impostazioni</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/ReaderPage.qml" line="57" />
|
||||
<location filename="../qml/pages/ReaderPage.qml" line="91"/>
|
||||
<source>Close book</source>
|
||||
<translation>Chiudi libro</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/ReaderPage.qml" line="170" />
|
||||
<source>The reader is not responding (WebView). The EPUB file may be damaged.</source>
|
||||
<translation>Il lettore non risponde (WebView). Il file EPUB potrebbe essere danneggiato.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/ReaderPage.qml" line="293" />
|
||||
<source>Chapter unreadable</source>
|
||||
<translation>Capitolo non leggibile</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/ReaderPage.qml" line="342" />
|
||||
<location filename="../qml/pages/ReaderPage.qml" line="368"/>
|
||||
<source>Unable to open the EPUB file</source>
|
||||
<translation>Impossibile aprire il libro EPUB</translation>
|
||||
</message>
|
||||
@@ -175,23 +166,23 @@
|
||||
<context>
|
||||
<name>SearchPage</name>
|
||||
<message>
|
||||
<location filename="../qml/pages/SearchPage.qml" line="15" />
|
||||
<location filename="../qml/pages/SearchPage.qml" line="27" />
|
||||
<location filename="../qml/pages/SearchPage.qml" line="17"/>
|
||||
<location filename="../qml/pages/SearchPage.qml" line="29"/>
|
||||
<source>Search</source>
|
||||
<translation>Cerca</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/SearchPage.qml" line="20" />
|
||||
<location filename="../qml/pages/SearchPage.qml" line="22"/>
|
||||
<source>Title or author…</source>
|
||||
<translation>Titolo o autore…</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/SearchPage.qml" line="35" />
|
||||
<location filename="../qml/pages/SearchPage.qml" line="37"/>
|
||||
<source>Search uses the server engine (OPDS search).</source>
|
||||
<translation>La ricerca usa il motore del server (OPDS search).</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/SearchPage.qml" line="50" />
|
||||
<location filename="../qml/pages/SearchPage.qml" line="52"/>
|
||||
<source>Results: </source>
|
||||
<translation>Risultati: </translation>
|
||||
</message>
|
||||
@@ -199,98 +190,83 @@
|
||||
<context>
|
||||
<name>SettingsPage</name>
|
||||
<message>
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="20" />
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="22"/>
|
||||
<source>Settings</source>
|
||||
<translation>Impostazioni</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="22" />
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="24"/>
|
||||
<source>Server</source>
|
||||
<translation>Server</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="28" />
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="30"/>
|
||||
<source>Calibre Web address</source>
|
||||
<translation>Indirizzo Calibre Web</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="39" />
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="41"/>
|
||||
<source>User (empty = anonymous access)</source>
|
||||
<translation>Utente (vuoto = accesso anonimo)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="48" />
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="50"/>
|
||||
<source>Password</source>
|
||||
<translation>Password</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="55" />
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="57"/>
|
||||
<source>Ignore SSL certificate errors</source>
|
||||
<translation>Ignora errori certificato SSL</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="56" />
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="58"/>
|
||||
<source>Enable if the server uses a self-signed certificate</source>
|
||||
<translation>Da attivare se il server usa un certificato autofirmato</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="61" />
|
||||
<source>Reader</source>
|
||||
<translation>Lettore</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="66" />
|
||||
<source>Side margins</source>
|
||||
<translation>Margini laterali</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="78" />
|
||||
<source>Top and bottom margin</source>
|
||||
<translation>Margine alto e basso</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="87" />
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="63"/>
|
||||
<source>Connection</source>
|
||||
<translation>Connessione</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="91" />
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="67"/>
|
||||
<source>Test connection</source>
|
||||
<translation>Prova connessione</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="115" />
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="120" />
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="91"/>
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="96"/>
|
||||
<source>Language</source>
|
||||
<translation type="finished">Lingua</translation>
|
||||
<translation>Lingua</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="123" />
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="99"/>
|
||||
<source>System (default)</source>
|
||||
<translation type="finished">Sistema (predefinita)</translation>
|
||||
<translation>Sistema (predefinita)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="131" />
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="107"/>
|
||||
<source>Language will be applied after restarting the app</source>
|
||||
<translation type="finished">La lingua verrà applicata al riavvio dell'app</translation>
|
||||
<translation>La lingua verrà applicata al riavvio dell'app</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="159" />
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="142"/>
|
||||
<source>Enter the server address</source>
|
||||
<translation>Inserisci l'indirizzo del server</translation>
|
||||
<translation>Inserisci l'indirizzo del server</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="156" />
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="132"/>
|
||||
<source>Version</source>
|
||||
<translation type="finished">Versione</translation>
|
||||
<translation>Versione</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="168" />
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="152"/>
|
||||
<source>Testing…</source>
|
||||
<translation>Verifica in corso…</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="179" />
|
||||
<location filename="../qml/pages/SettingsPage.qml" line="169"/>
|
||||
<source>Connection OK: %1 sections found</source>
|
||||
<translation>Connessione OK: %1 sezioni trovate</translation>
|
||||
</message>
|
||||
|
||||
Reference in New Issue
Block a user