feat(library): separate background texture for library and reader (#4754)

* feat(library): separate background texture for library and reader (#4743)

The library and reader shared a single background texture, so a reader
backdrop with borders or other reading-oriented decoration looked wrong
on the bookshelf. Let users set them independently.

- Add device-local libraryBackground{TextureId,Opacity,Size} to
  SystemSettings. Each field falls back to the reader/global value when
  unset (getLibraryViewSettings), so an existing bookshelf looks
  unchanged until the user picks a library texture, then decouples
  per-field. No migration needed; the selection stays per-device like
  the reader's, while imported images keep syncing via the texture kind.
- Make the Color panel's Background Image picker context-aware: opened
  from the library it edits the library texture, opened while reading it
  edits the reader texture. A sublabel states which page it applies to.
- Apply the library texture at boot and on every library mount, so
  returning from a textured book restores the bookshelf background.
- useBackgroundTexture now unmounts on 'none' instead of early-returning,
  since library and reader share one style element: switching a page to
  None must clear a texture the other page mounted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* i18n: translate library/reader background texture labels (#4743)

Add translations for the two new context sublabels ("Applies to the
Library" / "Applies to the Reader") across all 33 locales, anchored to
each locale's existing Library and reading terminology.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Huang Xin
2026-06-24 13:24:30 +08:00
committed by GitHub
parent e2f65278ec
commit 7d1a60b9ea
42 changed files with 309 additions and 66 deletions
@@ -1920,5 +1920,7 @@
"Purge & Delete": "مسح وحذف",
"Purge all reading data": "مسح جميع بيانات القراءة",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "يؤدي هذا إلى مسح تقدم القراءة والملاحظات والإشارات المرجعية نهائيًا. لا يمكن التراجع عن هذا الإجراء.",
"Also erase reading progress, notes, and bookmarks.": "يمسح أيضًا تقدم القراءة والملاحظات والإشارات المرجعية."
"Also erase reading progress, notes, and bookmarks.": "يمسح أيضًا تقدم القراءة والملاحظات والإشارات المرجعية.",
"Applies to the Library": "يُطبَّق على المكتبة",
"Applies to the Reader": "يُطبَّق على واجهة القراءة"
}
@@ -1788,5 +1788,7 @@
"Purge & Delete": "পরিষ্কার করে মুছুন",
"Purge all reading data": "সমস্ত পড়ার ডেটা পরিষ্কার করুন",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "এটি স্থায়ীভাবে পড়ার অগ্রগতি, নোট এবং বুকমার্ক মুছে ফেলে। এটি পূর্বাবস্থায় ফেরানো যাবে না।",
"Also erase reading progress, notes, and bookmarks.": "পড়ার অগ্রগতি, নোট এবং বুকমার্কও মুছে ফেলে।"
"Also erase reading progress, notes, and bookmarks.": "পড়ার অগ্রগতি, নোট এবং বুকমার্কও মুছে ফেলে।",
"Applies to the Library": "লাইব্রেরিতে প্রযোজ্য",
"Applies to the Reader": "রিডারে প্রযোজ্য"
}
@@ -1755,5 +1755,7 @@
"Purge & Delete": "གཙང་སེལ་བྱས་ནས་སུབ་པ།",
"Purge all reading data": "ཀློག་པའི་གཞི་གྲངས་ཡོངས་གཙང་སེལ།",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "འདིས་ཀློག་པའི་ཡར་འཕེལ་དང་། ཟིན་བྲིས། དཔེ་རྟགས་བཅས་གཏན་དུ་སུབ་ངེས་ཡིན། ཕྱིར་ལྡོག་བྱེད་མི་ཐུབ།",
"Also erase reading progress, notes, and bookmarks.": "ཀློག་པའི་ཡར་འཕེལ་དང་། ཟིན་བྲིས། དཔེ་རྟགས་བཅས་ཀྱང་སུབ་པ།"
"Also erase reading progress, notes, and bookmarks.": "ཀློག་པའི་ཡར་འཕེལ་དང་། ཟིན་བྲིས། དཔེ་རྟགས་བཅས་ཀྱང་སུབ་པ།",
"Applies to the Library": "དེབ་མཛོད་ལ་འཇུག་པ།",
"Applies to the Reader": "ཀློག་ངོས་ལ་འཇུག་པ།"
}
@@ -1788,5 +1788,7 @@
"Purge & Delete": "Bereinigen und löschen",
"Purge all reading data": "Alle Lesedaten bereinigen",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Dadurch werden Lesefortschritt, Notizen und Lesezeichen dauerhaft gelöscht. Dies kann nicht rückgängig gemacht werden.",
"Also erase reading progress, notes, and bookmarks.": "Löscht auch Lesefortschritt, Notizen und Lesezeichen."
"Also erase reading progress, notes, and bookmarks.": "Löscht auch Lesefortschritt, Notizen und Lesezeichen.",
"Applies to the Library": "Gilt für die Bibliothek",
"Applies to the Reader": "Gilt für die Leseansicht"
}
@@ -1788,5 +1788,7 @@
"Purge & Delete": "Εκκαθάριση & Διαγραφή",
"Purge all reading data": "Εκκαθάριση όλων των δεδομένων ανάγνωσης",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Αυτό διαγράφει οριστικά την πρόοδο ανάγνωσης, τις σημειώσεις και τους σελιδοδείκτες. Δεν είναι δυνατή η αναίρεση.",
"Also erase reading progress, notes, and bookmarks.": "Διαγράφει επίσης την πρόοδο ανάγνωσης, τις σημειώσεις και τους σελιδοδείκτες."
"Also erase reading progress, notes, and bookmarks.": "Διαγράφει επίσης την πρόοδο ανάγνωσης, τις σημειώσεις και τους σελιδοδείκτες.",
"Applies to the Library": "Ισχύει για τη βιβλιοθήκη",
"Applies to the Reader": "Ισχύει για την προβολή ανάγνωσης"
}
@@ -1821,5 +1821,7 @@
"Purge & Delete": "Purgar y eliminar",
"Purge all reading data": "Purgar todos los datos de lectura",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Esto borra permanentemente el progreso de lectura, las notas y los marcadores. Esta acción no se puede deshacer.",
"Also erase reading progress, notes, and bookmarks.": "También borra el progreso de lectura, las notas y los marcadores."
"Also erase reading progress, notes, and bookmarks.": "También borra el progreso de lectura, las notas y los marcadores.",
"Applies to the Library": "Se aplica a la biblioteca",
"Applies to the Reader": "Se aplica al lector"
}
@@ -1788,5 +1788,7 @@
"Purge & Delete": "پاک‌سازی و حذف",
"Purge all reading data": "پاک‌سازی همه داده‌های مطالعه",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "این کار وضعیت مطالعه، یادداشت‌ها و نشانک‌ها را برای همیشه پاک می‌کند. این عمل قابل بازگشت نیست.",
"Also erase reading progress, notes, and bookmarks.": "همچنین وضعیت مطالعه، یادداشت‌ها و نشانک‌ها را پاک می‌کند."
"Also erase reading progress, notes, and bookmarks.": "همچنین وضعیت مطالعه، یادداشت‌ها و نشانک‌ها را پاک می‌کند.",
"Applies to the Library": "برای کتابخانه اعمال می‌شود",
"Applies to the Reader": "برای صفحه مطالعه اعمال می‌شود"
}
@@ -1821,5 +1821,7 @@
"Purge & Delete": "Purger et supprimer",
"Purge all reading data": "Purger toutes les données de lecture",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Cela efface définitivement la progression de lecture, les notes et les signets. Cette action est irréversible.",
"Also erase reading progress, notes, and bookmarks.": "Efface aussi la progression de lecture, les notes et les signets."
"Also erase reading progress, notes, and bookmarks.": "Efface aussi la progression de lecture, les notes et les signets.",
"Applies to the Library": "Sapplique à la bibliothèque",
"Applies to the Reader": "Sapplique à la vue de lecture"
}
@@ -1821,5 +1821,7 @@
"Purge & Delete": "נקה ומחק",
"Purge all reading data": "נקה את כל נתוני הקריאה",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "פעולה זו מוחקת לצמיתות את התקדמות הקריאה, ההערות והסימניות. לא ניתן לבטל פעולה זו.",
"Also erase reading progress, notes, and bookmarks.": "מוחק גם את התקדמות הקריאה, ההערות והסימניות."
"Also erase reading progress, notes, and bookmarks.": "מוחק גם את התקדמות הקריאה, ההערות והסימניות.",
"Applies to the Library": "חל על הספרייה",
"Applies to the Reader": "חל על מצב הקריאה"
}
@@ -1788,5 +1788,7 @@
"Purge & Delete": "साफ़ करें और हटाएं",
"Purge all reading data": "सभी पढ़ने का डेटा साफ़ करें",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "यह पढ़ने की प्रगति, नोट्स और बुकमार्क को स्थायी रूप से मिटा देता है। इसे पूर्ववत नहीं किया जा सकता।",
"Also erase reading progress, notes, and bookmarks.": "पढ़ने की प्रगति, नोट्स और बुकमार्क भी मिटा देता है।"
"Also erase reading progress, notes, and bookmarks.": "पढ़ने की प्रगति, नोट्स और बुकमार्क भी मिटा देता है।",
"Applies to the Library": "लाइब्रेरी पर लागू होता है",
"Applies to the Reader": "रीडर पर लागू होता है"
}
@@ -1788,5 +1788,7 @@
"Purge & Delete": "Tisztítás és törlés",
"Purge all reading data": "Összes olvasási adat törlése",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Ez véglegesen törli az olvasási haladást, a jegyzeteket és a könyvjelzőket. A művelet nem vonható vissza.",
"Also erase reading progress, notes, and bookmarks.": "Az olvasási haladást, jegyzeteket és könyvjelzőket is törli."
"Also erase reading progress, notes, and bookmarks.": "Az olvasási haladást, jegyzeteket és könyvjelzőket is törli.",
"Applies to the Library": "A könyvtárra vonatkozik",
"Applies to the Reader": "Az olvasónézetre vonatkozik"
}
@@ -1755,5 +1755,7 @@
"Purge & Delete": "Bersihkan & Hapus",
"Purge all reading data": "Bersihkan semua data baca",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Ini menghapus permanen progres membaca, catatan, dan penanda. Tindakan ini tidak dapat dibatalkan.",
"Also erase reading progress, notes, and bookmarks.": "Juga menghapus progres membaca, catatan, dan penanda."
"Also erase reading progress, notes, and bookmarks.": "Juga menghapus progres membaca, catatan, dan penanda.",
"Applies to the Library": "Berlaku untuk Perpustakaan",
"Applies to the Reader": "Berlaku untuk tampilan baca"
}
@@ -1821,5 +1821,7 @@
"Purge & Delete": "Svuota ed elimina",
"Purge all reading data": "Elimina tutti i dati di lettura",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Questa operazione cancella definitivamente il progresso di lettura, le note e i segnalibri. Non può essere annullata.",
"Also erase reading progress, notes, and bookmarks.": "Cancella anche il progresso di lettura, le note e i segnalibri."
"Also erase reading progress, notes, and bookmarks.": "Cancella anche il progresso di lettura, le note e i segnalibri.",
"Applies to the Library": "Si applica alla libreria",
"Applies to the Reader": "Si applica alla vista di lettura"
}
@@ -1755,5 +1755,7 @@
"Purge & Delete": "消去して削除",
"Purge all reading data": "すべての読書データを消去",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "読書進捗、メモ、ブックマークが完全に消去されます。この操作は元に戻せません。",
"Also erase reading progress, notes, and bookmarks.": "読書進捗、メモ、ブックマークも消去します。"
"Also erase reading progress, notes, and bookmarks.": "読書進捗、メモ、ブックマークも消去します。",
"Applies to the Library": "ライブラリに適用されます",
"Applies to the Reader": "リーダーに適用されます"
}
@@ -1755,5 +1755,7 @@
"Purge & Delete": "데이터 지우고 삭제",
"Purge all reading data": "모든 읽기 데이터 지우기",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "읽기 진행 상황, 메모, 북마크가 영구적으로 지워집니다. 이 작업은 되돌릴 수 없습니다.",
"Also erase reading progress, notes, and bookmarks.": "읽기 진행 상황, 메모, 북마크도 함께 지웁니다."
"Also erase reading progress, notes, and bookmarks.": "읽기 진행 상황, 메모, 북마크도 함께 지웁니다.",
"Applies to the Library": "라이브러리에 적용됩니다",
"Applies to the Reader": "리더에 적용됩니다"
}
@@ -1755,5 +1755,7 @@
"Purge & Delete": "Bersihkan & Padam",
"Purge all reading data": "Bersihkan semua data bacaan",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Ini memadam kemajuan membaca, nota dan penanda buku secara kekal. Tindakan ini tidak boleh dibatalkan.",
"Also erase reading progress, notes, and bookmarks.": "Turut memadam kemajuan membaca, nota dan penanda buku."
"Also erase reading progress, notes, and bookmarks.": "Turut memadam kemajuan membaca, nota dan penanda buku.",
"Applies to the Library": "Digunakan pada Perpustakaan",
"Applies to the Reader": "Digunakan pada paparan bacaan"
}
@@ -1788,5 +1788,7 @@
"Purge & Delete": "Wissen en verwijderen",
"Purge all reading data": "Alle leesgegevens wissen",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Hiermee worden leesvoortgang, notities en bladwijzers permanent gewist. Dit kan niet ongedaan worden gemaakt.",
"Also erase reading progress, notes, and bookmarks.": "Wist ook leesvoortgang, notities en bladwijzers."
"Also erase reading progress, notes, and bookmarks.": "Wist ook leesvoortgang, notities en bladwijzers.",
"Applies to the Library": "Geldt voor de bibliotheek",
"Applies to the Reader": "Geldt voor de leesweergave"
}
@@ -1854,5 +1854,7 @@
"Purge & Delete": "Wyczyść i usuń",
"Purge all reading data": "Wyczyść wszystkie dane czytania",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Spowoduje to trwałe usunięcie postępu czytania, notatek i zakładek. Tej operacji nie można cofnąć.",
"Also erase reading progress, notes, and bookmarks.": "Usuwa również postęp czytania, notatki i zakładki."
"Also erase reading progress, notes, and bookmarks.": "Usuwa również postęp czytania, notatki i zakładki.",
"Applies to the Library": "Dotyczy biblioteki",
"Applies to the Reader": "Dotyczy widoku czytania"
}
@@ -1821,5 +1821,7 @@
"Purge & Delete": "Limpar e excluir",
"Purge all reading data": "Limpar todos os dados de leitura",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Isso apaga permanentemente o progresso de leitura, as notas e os marcadores. Esta ação não pode ser desfeita.",
"Also erase reading progress, notes, and bookmarks.": "Também apaga o progresso de leitura, as notas e os marcadores."
"Also erase reading progress, notes, and bookmarks.": "Também apaga o progresso de leitura, as notas e os marcadores.",
"Applies to the Library": "Aplica-se à biblioteca",
"Applies to the Reader": "Aplica-se à vista de leitura"
}
@@ -1821,5 +1821,7 @@
"Purge & Delete": "Limpar e excluir",
"Purge all reading data": "Limpar todos os dados de leitura",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Isto apaga permanentemente o progresso de leitura, notas e marcadores. Esta ação não pode ser desfeita.",
"Also erase reading progress, notes, and bookmarks.": "Também apaga o progresso de leitura, notas e marcadores."
"Also erase reading progress, notes, and bookmarks.": "Também apaga o progresso de leitura, notas e marcadores.",
"Applies to the Library": "Aplica-se à biblioteca",
"Applies to the Reader": "Aplica-se à vista de leitura"
}
@@ -1821,5 +1821,7 @@
"Purge & Delete": "Curăță și șterge",
"Purge all reading data": "Curăță toate datele de citire",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Aceasta șterge definitiv progresul citirii, notele și marcajele. Această acțiune nu poate fi anulată.",
"Also erase reading progress, notes, and bookmarks.": "Șterge de asemenea progresul citirii, notele și marcajele."
"Also erase reading progress, notes, and bookmarks.": "Șterge de asemenea progresul citirii, notele și marcajele.",
"Applies to the Library": "Se aplică bibliotecii",
"Applies to the Reader": "Se aplică vizualizării de lectură"
}
@@ -1854,5 +1854,7 @@
"Purge & Delete": "Очистить и удалить",
"Purge all reading data": "Очистить все данные чтения",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Это безвозвратно удалит прогресс чтения, заметки и закладки. Это действие нельзя отменить.",
"Also erase reading progress, notes, and bookmarks.": "Также удаляет прогресс чтения, заметки и закладки."
"Also erase reading progress, notes, and bookmarks.": "Также удаляет прогресс чтения, заметки и закладки.",
"Applies to the Library": "Применяется к библиотеке",
"Applies to the Reader": "Применяется к режиму чтения"
}
@@ -1788,5 +1788,7 @@
"Purge & Delete": "ඉවත් කර මකන්න",
"Purge all reading data": "සියලු කියවීම් දත්ත ඉවත් කරන්න",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "මෙය කියවීමේ ප්‍රගතිය, සටහන් සහ පොත් සලකුණු ස්ථිරවම මකා දමයි. මෙය අහෝසි කළ නොහැක.",
"Also erase reading progress, notes, and bookmarks.": "කියවීමේ ප්‍රගතිය, සටහන් සහ පොත් සලකුණු ද මකා දමයි."
"Also erase reading progress, notes, and bookmarks.": "කියවීමේ ප්‍රගතිය, සටහන් සහ පොත් සලකුණු ද මකා දමයි.",
"Applies to the Library": "පුස්තකාලයට අදාළ වේ",
"Applies to the Reader": "කියවීමේ දසුනට අදාළ වේ"
}
@@ -1854,5 +1854,7 @@
"Purge & Delete": "Počisti in izbriši",
"Purge all reading data": "Počisti vse podatke o branju",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "To trajno izbriše napredek branja, opombe in zaznamke. Tega ni mogoče razveljaviti.",
"Also erase reading progress, notes, and bookmarks.": "Izbriše tudi napredek branja, opombe in zaznamke."
"Also erase reading progress, notes, and bookmarks.": "Izbriše tudi napredek branja, opombe in zaznamke.",
"Applies to the Library": "Velja za knjižnico",
"Applies to the Reader": "Velja za bralni pogled"
}
@@ -1788,5 +1788,7 @@
"Purge & Delete": "Rensa och ta bort",
"Purge all reading data": "Rensa alla läsdata",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Detta raderar permanent läsframsteg, anteckningar och bokmärken. Detta kan inte ångras.",
"Also erase reading progress, notes, and bookmarks.": "Raderar även läsframsteg, anteckningar och bokmärken."
"Also erase reading progress, notes, and bookmarks.": "Raderar även läsframsteg, anteckningar och bokmärken.",
"Applies to the Library": "Gäller biblioteket",
"Applies to the Reader": "Gäller läsvyn"
}
@@ -1788,5 +1788,7 @@
"Purge & Delete": "அழித்து நீக்கு",
"Purge all reading data": "அனைத்து வாசிப்புத் தரவையும் அழி",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "இது வாசிப்பு முன்னேற்றம், குறிப்புகள் மற்றும் புக்மார்க்குகளை நிரந்தரமாக அழிக்கும். இதை மீட்டெடுக்க முடியாது.",
"Also erase reading progress, notes, and bookmarks.": "வாசிப்பு முன்னேற்றம், குறிப்புகள் மற்றும் புக்மார்க்குகளையும் அழிக்கும்."
"Also erase reading progress, notes, and bookmarks.": "வாசிப்பு முன்னேற்றம், குறிப்புகள் மற்றும் புக்மார்க்குகளையும் அழிக்கும்.",
"Applies to the Library": "நூலகத்திற்குப் பொருந்தும்",
"Applies to the Reader": "வாசிப்புப் பக்கத்திற்குப் பொருந்தும்"
}
@@ -1755,5 +1755,7 @@
"Purge & Delete": "ล้างและลบ",
"Purge all reading data": "ล้างข้อมูลการอ่านทั้งหมด",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "การดำเนินการนี้จะลบความคืบหน้าในการอ่าน บันทึก และบุ๊กมาร์กอย่างถาวร ไม่สามารถเลิกทำได้",
"Also erase reading progress, notes, and bookmarks.": "ลบความคืบหน้าในการอ่าน บันทึก และบุ๊กมาร์กด้วย"
"Also erase reading progress, notes, and bookmarks.": "ลบความคืบหน้าในการอ่าน บันทึก และบุ๊กมาร์กด้วย",
"Applies to the Library": "ใช้กับคลังหนังสือ",
"Applies to the Reader": "ใช้กับมุมมองการอ่าน"
}
@@ -1788,5 +1788,7 @@
"Purge & Delete": "Temizle ve sil",
"Purge all reading data": "Tüm okuma verilerini temizle",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Bu, okuma ilerlemesini, notları ve yer işaretlerini kalıcı olarak siler. Bu işlem geri alınamaz.",
"Also erase reading progress, notes, and bookmarks.": "Okuma ilerlemesi, notlar ve yer işaretlerini de siler."
"Also erase reading progress, notes, and bookmarks.": "Okuma ilerlemesi, notlar ve yer işaretlerini de siler.",
"Applies to the Library": "Kütüphaneye uygulanır",
"Applies to the Reader": "Okuma görünümüne uygulanır"
}
@@ -1854,5 +1854,7 @@
"Purge & Delete": "Очистити та видалити",
"Purge all reading data": "Очистити всі дані читання",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Це назавжди видалить проґрес читання, нотатки та закладки. Цю дію не можна скасувати.",
"Also erase reading progress, notes, and bookmarks.": "Також видаляє проґрес читання, нотатки та закладки."
"Also erase reading progress, notes, and bookmarks.": "Також видаляє проґрес читання, нотатки та закладки.",
"Applies to the Library": "Застосовується до бібліотеки",
"Applies to the Reader": "Застосовується до режиму читання"
}
@@ -1788,5 +1788,7 @@
"Purge & Delete": "Tozalash va oʻchirish",
"Purge all reading data": "Barcha oʻqish maʼlumotlarini tozalash",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Bu oʻqish jarayoni, eslatmalar va xatchoʻplarni butunlay oʻchiradi. Buni qaytarib boʻlmaydi.",
"Also erase reading progress, notes, and bookmarks.": "Shuningdek, oʻqish jarayoni, eslatmalar va xatchoʻplarni ham oʻchiradi."
"Also erase reading progress, notes, and bookmarks.": "Shuningdek, oʻqish jarayoni, eslatmalar va xatchoʻplarni ham oʻchiradi.",
"Applies to the Library": "Kutubxonaga qoʻllanadi",
"Applies to the Reader": "Oʻqish koʻrinishiga qoʻllanadi"
}
@@ -1755,5 +1755,7 @@
"Purge & Delete": "Dọn sạch và xóa",
"Purge all reading data": "Xóa toàn bộ dữ liệu đọc",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Thao tác này xóa vĩnh viễn tiến độ đọc, ghi chú và dấu trang. Không thể hoàn tác.",
"Also erase reading progress, notes, and bookmarks.": "Đồng thời xóa tiến độ đọc, ghi chú và dấu trang."
"Also erase reading progress, notes, and bookmarks.": "Đồng thời xóa tiến độ đọc, ghi chú và dấu trang.",
"Applies to the Library": "Áp dụng cho Thư viện",
"Applies to the Reader": "Áp dụng cho trình đọc"
}
@@ -1755,5 +1755,7 @@
"Purge & Delete": "清除并删除",
"Purge all reading data": "清除所有阅读数据",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "这将永久清除阅读进度、笔记和书签,且无法撤销。",
"Also erase reading progress, notes, and bookmarks.": "同时清除阅读进度、笔记和书签。"
"Also erase reading progress, notes, and bookmarks.": "同时清除阅读进度、笔记和书签。",
"Applies to the Library": "应用于书库",
"Applies to the Reader": "应用于阅读界面"
}
@@ -1755,5 +1755,7 @@
"Purge & Delete": "清除並刪除",
"Purge all reading data": "清除所有閱讀資料",
"This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "這將永久清除閱讀進度、筆記和書籤,且無法復原。",
"Also erase reading progress, notes, and bookmarks.": "同時清除閱讀進度、筆記和書籤。"
"Also erase reading progress, notes, and bookmarks.": "同時清除閱讀進度、筆記和書籤。",
"Applies to the Library": "套用於書庫",
"Applies to the Reader": "套用於閱讀介面"
}
@@ -32,7 +32,7 @@ vi.mock('@/utils/style', () => ({
getStyles: vi.fn(() => ''),
}));
import { saveViewSettings } from '@/helpers/settings';
import { getLibraryViewSettings, saveViewSettings } from '@/helpers/settings';
import { useSettingsStore } from '@/store/settingsStore';
import type { EnvConfigType } from '@/services/environment';
import type { SystemSettings } from '@/types/settings';
@@ -55,6 +55,61 @@ beforeEach(() => {
} as unknown as ReturnType<typeof useSettingsStore.getState>);
});
describe('getLibraryViewSettings', () => {
const makeTextureSettings = (overrides: Partial<SystemSettings> = {}): SystemSettings =>
({
globalViewSettings: {
backgroundTextureId: 'paper',
backgroundOpacity: 0.6,
backgroundSize: 'cover',
},
...overrides,
}) as unknown as SystemSettings;
test('inherits the reader/global texture when no library override is set', () => {
const result = getLibraryViewSettings(makeTextureSettings());
expect(result.backgroundTextureId).toBe('paper');
expect(result.backgroundOpacity).toBe(0.6);
expect(result.backgroundSize).toBe('cover');
});
test('uses the library overrides when they are set', () => {
const result = getLibraryViewSettings(
makeTextureSettings({
libraryBackgroundTextureId: 'none',
libraryBackgroundOpacity: 0.3,
libraryBackgroundSize: 'contain',
}),
);
expect(result.backgroundTextureId).toBe('none');
expect(result.backgroundOpacity).toBe(0.3);
expect(result.backgroundSize).toBe('contain');
});
test("tolerates the store's initial empty settings (no globalViewSettings yet)", () => {
// useSettingsStore starts as `{} as SystemSettings`; the library page's
// texture effect can resolve before appService.loadSettings() populates it.
// It must yield a usable "no texture" result instead of throwing.
const result = getLibraryViewSettings({} as SystemSettings);
expect(result.backgroundTextureId).toBe('none');
});
test('resolves each field independently — an unset field still inherits', () => {
// Only the texture id is decoupled; opacity/size were never touched and
// must keep tracking the reader/global values.
const result = getLibraryViewSettings(
makeTextureSettings({ libraryBackgroundTextureId: 'sand' }),
);
expect(result.backgroundTextureId).toBe('sand');
expect(result.backgroundOpacity).toBe(0.6);
expect(result.backgroundSize).toBe('cover');
});
});
describe('saveViewSettings', () => {
test('global write swaps the settings reference so replicaSettingsSync subscribers fire', async () => {
// Mirrors the gating subscriber installed by replicaSettingsSync.initSettingsSync.
@@ -107,4 +107,25 @@ describe('useBackgroundTexture', () => {
expect(useCustomTextureStore.getState().getTexture(SAVED_TEXTURE.id)).toBeUndefined();
});
test('unmounts any mounted texture when textureId is "none"', async () => {
// Library/reader share the single #background-texture style element. When a
// page whose texture is "none" becomes active (e.g. returning to a
// None-background library after reading a textured book), applying it must
// actively clear the leftover texture, not silently leave it mounted.
const { mountBackgroundTexture, unmountBackgroundTexture } = await import('@/styles/textures');
vi.mocked(mountBackgroundTexture).mockClear();
vi.mocked(unmountBackgroundTexture).mockClear();
const { result } = renderHook(() => useBackgroundTexture());
result.current.applyBackgroundTexture(
createMockEnvConfig(),
makeViewSettings({ backgroundTextureId: 'none' }),
);
await vi.waitFor(() => {
expect(unmountBackgroundTexture).toHaveBeenCalled();
});
expect(mountBackgroundTexture).not.toHaveBeenCalled();
});
});
+21
View File
@@ -41,6 +41,8 @@ import { useOPDSSubscriptions } from '@/hooks/useOPDSSubscriptions';
import { useBookDataStore } from '@/store/bookDataStore';
import { useTransferStore } from '@/store/transferStore';
import { useScreenWakeLock } from '@/hooks/useScreenWakeLock';
import { useBackgroundTexture } from '@/hooks/useBackgroundTexture';
import { getLibraryViewSettings } from '@/helpers/settings';
import { useAppUrlIngress } from '@/hooks/useAppUrlIngress';
import { useOpenWithBooks } from '@/hooks/useOpenWithBooks';
import { useOpenAnnotationLink } from '@/hooks/useOpenAnnotationLink';
@@ -241,6 +243,25 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
useTheme({ systemUIVisible: true, appThemeColor: 'base-200' });
useUICSS();
// Apply the library's own background texture (separate from the reader's,
// issue #4743). Re-applies on mount so returning from a textured book
// restores the library background, and whenever the library texture — or the
// reader/global texture it inherits when unset — changes from the Color panel.
const { applyBackgroundTexture } = useBackgroundTexture();
useEffect(() => {
applyBackgroundTexture(envConfig, getLibraryViewSettings(settings));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
envConfig,
applyBackgroundTexture,
settings.libraryBackgroundTextureId,
settings.libraryBackgroundOpacity,
settings.libraryBackgroundSize,
settings.globalViewSettings?.backgroundTextureId,
settings.globalViewSettings?.backgroundOpacity,
settings.globalViewSettings?.backgroundSize,
]);
useAppUrlIngress();
useOpenWithBooks();
useOpenAnnotationLink();
@@ -25,6 +25,7 @@ import {
setTelemetryDecision,
TELEMETRY_OPT_OUT_KEY,
} from '@/utils/telemetry';
import { getLibraryViewSettings } from '@/helpers/settings';
import { SETTINGS_FILENAME } from '@/services/constants';
import type { AppService } from '@/types/system';
import type { SystemSettings } from '@/types/settings';
@@ -158,7 +159,10 @@ const Providers = ({ children }: { children: React.ReactNode }) => {
if (settings.customTextures?.length) {
useCustomTextureStore.getState().setTextures(settings.customTextures);
}
applyBackgroundTexture(envConfig, globalViewSettings);
// The app boots onto the library, so apply the library background
// (which inherits the reader/global texture until decoupled). The
// reader re-applies its own texture when a book opens (issue #4743).
applyBackgroundTexture(envConfig, getLibraryViewSettings(settings));
if (globalViewSettings.isEink) {
applyEinkMode(true);
}
@@ -15,7 +15,7 @@ import { useSettingsStore } from '@/store/settingsStore';
import { useResetViewSettings } from '@/hooks/useResetSettings';
import { useCustomTextureStore } from '@/store/customTextureStore';
import { queueReplicaBinaryUpload } from '@/services/sync/replicaBinaryUpload';
import { saveViewSettings } from '@/helpers/settings';
import { saveSysSettings, saveViewSettings } from '@/helpers/settings';
import { manageSyntaxHighlighting } from '@/utils/highlightjs';
import { SettingsPanelPanelProp } from './SettingsDialog';
import { useFileSelector } from '@/hooks/useFileSelector';
@@ -42,6 +42,21 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
const { getView, getViewSettings } = useReaderStore();
const viewSettings = getViewSettings(bookKey) || settings.globalViewSettings;
// The Background Image picker is context-aware (issue #4743): opened from the
// library (no bookKey) it edits the library's own texture, which falls back
// to the reader/global value per-field until decoupled; opened while reading
// it edits the reader texture exactly as before.
const isLibraryContext = !bookKey;
const currentTextureId = isLibraryContext
? (settings.libraryBackgroundTextureId ?? viewSettings.backgroundTextureId)
: viewSettings.backgroundTextureId;
const currentBackgroundOpacity = isLibraryContext
? (settings.libraryBackgroundOpacity ?? viewSettings.backgroundOpacity)
: viewSettings.backgroundOpacity;
const currentBackgroundSize = isLibraryContext
? (settings.libraryBackgroundSize ?? viewSettings.backgroundSize)
: viewSettings.backgroundSize;
const [invertImgColorInDark, setInvertImgColorInDark] = useState(
viewSettings.invertImgColorInDark,
);
@@ -51,9 +66,9 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
const [overrideColor, setOverrideColor] = useState(viewSettings.overrideColor);
const [codeHighlighting, setcodeHighlighting] = useState(viewSettings.codeHighlighting);
const [codeLanguage, setCodeLanguage] = useState(viewSettings.codeLanguage);
const [selectedTextureId, setSelectedTextureId] = useState(viewSettings.backgroundTextureId);
const [backgroundOpacity, setBackgroundOpacity] = useState(viewSettings.backgroundOpacity);
const [backgroundSize, setBackgroundSize] = useState(viewSettings.backgroundSize);
const [selectedTextureId, setSelectedTextureId] = useState(currentTextureId);
const [backgroundOpacity, setBackgroundOpacity] = useState(currentBackgroundOpacity);
const [backgroundSize, setBackgroundSize] = useState(currentBackgroundSize);
const [highlightOpacity, setHighlightOpacity] = useState(viewSettings.highlightOpacity ?? 0.3);
const [customHighlightColors, setCustomHighlightColors] = useState(
settings.globalReadSettings.customHighlightColors,
@@ -161,22 +176,34 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
}, [codeHighlighting, codeLanguage]);
useEffect(() => {
if (selectedTextureId === viewSettings.backgroundTextureId) return;
saveViewSettings(envConfig, bookKey, 'backgroundTextureId', selectedTextureId);
if (selectedTextureId === currentTextureId) return;
if (isLibraryContext) {
saveSysSettings(envConfig, 'libraryBackgroundTextureId', selectedTextureId);
} else {
saveViewSettings(envConfig, bookKey, 'backgroundTextureId', selectedTextureId);
}
applyBackgroundTexture();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedTextureId]);
useEffect(() => {
if (backgroundOpacity === viewSettings.backgroundOpacity) return;
saveViewSettings(envConfig, bookKey, 'backgroundOpacity', backgroundOpacity);
if (backgroundOpacity === currentBackgroundOpacity) return;
if (isLibraryContext) {
saveSysSettings(envConfig, 'libraryBackgroundOpacity', backgroundOpacity);
} else {
saveViewSettings(envConfig, bookKey, 'backgroundOpacity', backgroundOpacity);
}
applyBackgroundTexture();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [backgroundOpacity]);
useEffect(() => {
if (backgroundSize === viewSettings.backgroundSize) return;
saveViewSettings(envConfig, bookKey, 'backgroundSize', backgroundSize);
if (backgroundSize === currentBackgroundSize) return;
if (isLibraryContext) {
saveSysSettings(envConfig, 'libraryBackgroundSize', backgroundSize);
} else {
saveViewSettings(envConfig, bookKey, 'backgroundSize', backgroundSize);
}
applyBackgroundTexture();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [backgroundSize]);
@@ -364,6 +391,7 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
<BackgroundTextureSelector
predefinedTextures={PREDEFINED_TEXTURES}
customTextures={customTextures.filter((t) => !t.deletedAt)}
scopeLabel={isLibraryContext ? _('Applies to the Library') : _('Applies to the Reader')}
selectedTextureId={selectedTextureId}
backgroundOpacity={backgroundOpacity}
backgroundSize={backgroundSize}
@@ -15,6 +15,8 @@ interface Texture {
interface BackgroundTextureSelectorProps {
predefinedTextures: Texture[];
customTextures: Texture[];
/** Optional sublabel clarifying which page this texture applies to (#4743). */
scopeLabel?: string;
selectedTextureId: string;
backgroundOpacity: number;
backgroundSize: string;
@@ -28,6 +30,7 @@ interface BackgroundTextureSelectorProps {
const BackgroundTextureSelector: React.FC<BackgroundTextureSelectorProps> = ({
predefinedTextures,
customTextures,
scopeLabel,
selectedTextureId,
backgroundOpacity,
backgroundSize,
@@ -44,7 +47,8 @@ const BackgroundTextureSelector: React.FC<BackgroundTextureSelectorProps> = ({
return (
<div>
<SectionTitle className='mb-2'>{_('Background Image')}</SectionTitle>
<SectionTitle className={scopeLabel ? 'mb-1' : 'mb-2'}>{_('Background Image')}</SectionTitle>
{scopeLabel && <p className='text-base-content/60 mb-2 text-xs'>{scopeLabel}</p>}
<div className='mb-4 grid grid-cols-2 gap-4'>
{allTextures.map((texture) => (
// The swatch is a div (not a <button>) so the inner Delete
+23
View File
@@ -6,6 +6,29 @@ import { useReaderStore } from '@/store/readerStore';
import { useSettingsStore } from '@/store/settingsStore';
import { getStyles } from '@/utils/style';
/**
* Resolve the effective background texture for the library page (issue #4743).
* The library texture is stored separately from the reader's, but each field
* falls back to the reader/global value when unset so the bookshelf inherits
* the current look until the user explicitly picks a library texture, then
* decouples per-field. Returns a `ViewSettings` so it can be handed straight to
* `useBackgroundTexture().applyBackgroundTexture`.
*/
export const getLibraryViewSettings = (settings: SystemSettings): ViewSettings => {
// globalViewSettings can be absent on the very first renders — the store
// starts as `{} as SystemSettings` until appService.loadSettings() runs — so
// every read is optional and falls back to a no-texture default.
const globalViewSettings = settings.globalViewSettings;
return {
...globalViewSettings,
backgroundTextureId:
settings.libraryBackgroundTextureId ?? globalViewSettings?.backgroundTextureId ?? 'none',
backgroundOpacity:
settings.libraryBackgroundOpacity ?? globalViewSettings?.backgroundOpacity ?? 0.6,
backgroundSize: settings.libraryBackgroundSize ?? globalViewSettings?.backgroundSize ?? 'cover',
};
};
export const saveViewSettings = async <K extends keyof ViewSettings>(
envConfig: EnvConfigType,
bookKey: string,
@@ -7,31 +7,40 @@ import { ViewSettings } from '@/types/book';
export const useBackgroundTexture = () => {
const applyBackgroundTexture = useCallback(
(envConfig: EnvConfigType, viewSettings: ViewSettings) => {
const textureId = viewSettings.backgroundTextureId;
const textureOpacity = viewSettings.backgroundOpacity;
const textureSize = viewSettings.backgroundSize;
if (!textureId || textureId === 'none') return;
const textureId = viewSettings.backgroundTextureId || 'none';
document.documentElement.style.setProperty('--bg-texture-opacity', `${textureOpacity}`);
document.documentElement.style.setProperty('--bg-texture-size', textureSize);
if (textureId !== 'none') {
document.documentElement.style.setProperty(
'--bg-texture-opacity',
`${viewSettings.backgroundOpacity}`,
);
document.documentElement.style.setProperty(
'--bg-texture-size',
viewSettings.backgroundSize,
);
const settings = useSettingsStore.getState().settings;
const customTexture = settings.customTextures?.find((t) => t.id === textureId);
const settings = useSettingsStore.getState().settings;
const customTexture = settings.customTextures?.find((t) => t.id === textureId);
if (customTexture) {
// Carry replica-sync metadata (contentId / bundleDir / byteSize)
// through addTexture so the boot-time "ensure selected texture
// is in the store" path doesn't drop them and silently un-
// publish a remote-pulled record.
useCustomTextureStore.getState().addTexture(customTexture.path, {
name: customTexture.name,
contentId: customTexture.contentId,
bundleDir: customTexture.bundleDir,
byteSize: customTexture.byteSize,
animated: customTexture.animated,
});
if (customTexture) {
// Carry replica-sync metadata (contentId / bundleDir / byteSize)
// through addTexture so the boot-time "ensure selected texture
// is in the store" path doesn't drop them and silently un-
// publish a remote-pulled record.
useCustomTextureStore.getState().addTexture(customTexture.path, {
name: customTexture.name,
contentId: customTexture.contentId,
bundleDir: customTexture.bundleDir,
byteSize: customTexture.byteSize,
animated: customTexture.animated,
});
}
}
// Always delegate to applyTexture: for a real texture it mounts/loads,
// for 'none' it unmounts. The library and reader share one
// #background-texture style element, so switching to a 'none' page must
// actively clear a texture the other page mounted (issue #4743).
useCustomTextureStore.getState().applyTexture(envConfig, textureId);
},
[],
+12
View File
@@ -324,6 +324,18 @@ export interface SystemSettings {
libraryCoverFit: LibraryCoverFitType;
libraryAutoColumns: boolean;
libraryColumns: number;
/**
* Library page background texture, configured independently from the reader
* background (issue #4743). When any of these is undefined the library
* inherits the corresponding `globalViewSettings.background*` value, so an
* existing user's bookshelf looks unchanged until they pick a library
* texture. Device-local (the texture *selection* never syncs, matching the
* reader's `backgroundTextureId`); only the imported image binaries sync via
* the `texture` replica kind. Resolved by `getLibraryViewSettings`.
*/
libraryBackgroundTextureId?: string;
libraryBackgroundOpacity?: number;
libraryBackgroundSize?: string;
customFonts: CustomFont[];
customTextures: CustomTexture[];
customDictionaries: ImportedDictionary[];