feat: enhance Komga integration with API key updates and user management improvements closes [BUG] Komga Setup Returning a 401 error when Komga is set up and working fine

Fixes #693
This commit is contained in:
Matthieu B
2025-10-06 12:32:37 +02:00
parent 65fd09e622
commit 8c18977094
11 changed files with 322 additions and 170 deletions

View File

@@ -421,6 +421,8 @@ def image_proxy():
headers["X-Emby-Token"] = server.api_key
elif server.server_type == "plex":
headers["X-Plex-Token"] = server.api_key
elif server.server_type == "komga":
headers["X-API-Key"] = server.api_key
# Fetch the image
r = requests.get(url, headers=headers, timeout=10, stream=True)

View File

@@ -30,7 +30,7 @@ class KomgaClient(RestApiMixin):
def _headers(self) -> dict[str, str]:
headers = {"Accept": "application/json"}
if self.token:
headers["Authorization"] = f"Bearer {self.token}"
headers["X-API-Key"] = self.token
return headers
def libraries(self) -> dict[str, str]:
@@ -57,7 +57,7 @@ class KomgaClient(RestApiMixin):
"""
try:
if url and token:
headers = {"Authorization": f"Bearer {token}"}
headers = {"X-API-Key": token}
response = requests.get(
f"{url.rstrip('/')}/api/v1/libraries", headers=headers, timeout=10
)
@@ -72,15 +72,31 @@ class KomgaClient(RestApiMixin):
logging.warning("Komga: failed to scan libraries %s", exc)
return {}
def create_user(self, username: str, password: str, email: str) -> str:
"""Create a new Komga user and return the user ID."""
payload = {"email": email, "password": password, "roles": ["USER"]}
response = self.post("/api/v1/users", json=payload)
def create_user(
self, username: str, password: str, email: str, allow_downloads: bool = False
) -> str:
"""Create a new Komga user and return the user ID.
Args:
username: Username for the new user (not used by Komga, only email)
password: Password for the new user
email: Email address for the new user
allow_downloads: Whether to grant FILE_DOWNLOAD role
Returns:
str: The new user's ID
"""
roles = ["USER"]
if allow_downloads:
roles.append("FILE_DOWNLOAD")
payload = {"email": email, "password": password, "roles": roles}
response = self.post("/api/v2/users", json=payload)
return response.json()["id"]
def update_user(self, user_id: str, updates: dict[str, Any]) -> dict[str, Any]:
"""Update a Komga user."""
response = self.patch(f"/api/v1/users/{user_id}", json=updates)
response = self.patch(f"/api/v2/users/{user_id}", json=updates)
return response.json()
def disable_user(self, user_id: str) -> bool:
@@ -104,7 +120,7 @@ class KomgaClient(RestApiMixin):
def delete_user(self, user_id: str) -> None:
"""Delete a Komga user."""
self.delete(f"/api/v1/users/{user_id}")
self.delete(f"/api/v2/users/{user_id}")
def get_user(self, user_id: str) -> dict[str, Any]:
"""Get user info in legacy format for backward compatibility."""
@@ -132,18 +148,35 @@ class KomgaClient(RestApiMixin):
)
# Get raw user data from Komga API
response = self.get(f"/api/v1/users/{user_id}")
response = self.get(f"/api/v2/users/{user_id}")
raw_user = response.json()
# Extract permissions using utility
roles = raw_user.get("roles", [])
permissions = StandardizedPermissions.for_basic_server(
"komga",
is_admin="ADMIN" in raw_user.get("roles", []),
allow_downloads=True, # Comic reader allows downloads
is_admin="ADMIN" in roles,
allow_downloads="FILE_DOWNLOAD" in roles,
)
# Komga gives full access to all libraries
library_access = LibraryAccessHelper.create_full_access()
# Handle library access - always return actual library names
if raw_user.get("sharedAllLibraries", False):
# User has access to all libraries - fetch all library IDs from server
all_libraries = self.libraries() # Returns {id: name} mapping
shared_library_ids = list(all_libraries.keys())
else:
# User has restricted library access
shared_library_ids = raw_user.get("sharedLibrariesIds", [])
# Always create restricted access with the actual library IDs
library_access = (
LibraryAccessHelper.create_restricted_access(
shared_library_ids, getattr(self, "server_id", None)
)
if shared_library_ids
else []
)
# Parse dates
created_at = DateHelper.parse_iso_date(raw_user.get("createdDate"))
@@ -164,7 +197,7 @@ class KomgaClient(RestApiMixin):
def list_users(self) -> list[User]:
"""Sync users from Komga into the local DB and return the list of User records."""
try:
response = self.get("/api/v1/users")
response = self.get("/api/v2/users")
komga_users = {u["id"]: u for u in response.json()}
for komga_user in komga_users.values():
@@ -193,23 +226,35 @@ class KomgaClient(RestApiMixin):
User.server_id == getattr(self, "server_id", None)
).all()
# Add default policy attributes (Komga doesn't have specific download/live TV policies)
# Add policy attributes including library access from Komga
for user in users:
# Get the full user data from Komga to extract library info
komga_user_data = komga_users.get(user.token, {})
roles = komga_user_data.get("roles", [])
# Check for FILE_DOWNLOAD role to determine download permission
allow_downloads = "FILE_DOWNLOAD" in roles
# Store both server-specific and standardized keys in policies dict
komga_policies = {
# Server-specific data (Komga user info would go here)
# Server-specific data (Komga user info)
"enabled": True, # Komga users are enabled by default
"sharedAllLibraries": komga_user_data.get(
"sharedAllLibraries", False
),
"sharedLibrariesIds": komga_user_data.get("sharedLibrariesIds", []),
"roles": roles,
# Standardized permission keys for UI display
"allow_downloads": True, # Default to True for reading apps
"allow_downloads": allow_downloads,
"allow_live_tv": False, # Komga doesn't have Live TV
"allow_sync": True, # Default to True for reading apps
}
user.set_raw_policies(komga_policies)
# Update standardized User model columns
user.allow_downloads = True # Default for reading apps
user.allow_downloads = allow_downloads
user.allow_live_tv = False # Komga doesn't have Live TV
user.is_admin = False # Would need API call to determine
user.is_admin = "ADMIN" in roles
# Single commit for all metadata updates
try:
@@ -219,19 +264,116 @@ class KomgaClient(RestApiMixin):
db.session.rollback()
return []
# Cache detailed metadata for all users (including library access)
self._cache_user_metadata_from_bulk_response(users, komga_users)
# Commit the standardized metadata updates
try:
db.session.commit()
except Exception as e:
logging.error(
f"Komga: failed to commit standardized metadata updates: {e}"
)
db.session.rollback()
return users
except Exception as e:
logging.error(f"Failed to list Komga users: {e}")
return []
def _cache_user_metadata_from_bulk_response(
self, users: list[User], komga_users: dict
) -> None:
"""Cache user metadata from bulk API response without individual API calls.
Args:
users: List of User objects to cache metadata for
komga_users: Dictionary of raw user data from bulk /api/v2/users response
"""
if not users or not komga_users:
return
from app.services.media.utils import (
DateHelper,
LibraryAccessHelper,
StandardizedPermissions,
create_standardized_user_details,
)
cached_count = 0
for user in users:
try:
# Get the raw user data from bulk response
raw_user = komga_users.get(user.token)
if not raw_user:
continue
roles = raw_user.get("roles", [])
# Extract standardized permissions
permissions = StandardizedPermissions.for_basic_server(
"komga",
is_admin="ADMIN" in roles,
allow_downloads="FILE_DOWNLOAD" in roles,
)
# Handle library access - always return actual library names
if raw_user.get("sharedAllLibraries", False):
# User has access to all libraries - fetch all library IDs from server
all_libraries = self.libraries() # Returns {id: name} mapping
shared_library_ids = list(all_libraries.keys())
else:
# User has restricted library access
shared_library_ids = raw_user.get("sharedLibrariesIds", [])
# Always create restricted access with the actual library IDs
library_access = (
LibraryAccessHelper.create_restricted_access(
shared_library_ids, getattr(self, "server_id", None)
)
if shared_library_ids
else []
)
# Parse dates
created_at = DateHelper.parse_iso_date(raw_user.get("createdDate"))
last_active = DateHelper.parse_iso_date(raw_user.get("lastActiveDate"))
# Create standardized user details
details = create_standardized_user_details(
user_id=user.token,
username=raw_user.get("email", "Unknown"),
email=raw_user.get("email"),
permissions=permissions,
library_access=library_access,
raw_policies=raw_user,
created_at=created_at,
last_active=last_active,
is_enabled=True, # Komga doesn't have a disabled state
)
# Update the standardized metadata columns in the User record
user.update_standardized_metadata(details)
cached_count += 1
except Exception as e:
logging.warning(
f"Failed to cache metadata for Komga user {user.token}: {e}"
)
continue
if cached_count > 0:
logging.info(f"Cached metadata for {cached_count} Komga users")
def _set_library_access(self, user_id: str, library_ids: list[str]) -> None:
"""Set library access for a user."""
if not library_ids:
return
try:
for library_id in library_ids:
self.put(f"/api/v1/users/{user_id}/shared-libraries/{library_id}")
# Use v2 API to set library access via user update
updates = {"sharedLibraries": {"all": False, "libraryIds": library_ids}}
self.patch(f"/api/v2/users/{user_id}", json=updates)
except Exception as e:
logging.warning(f"Failed to set library access for user {user_id}: {e}")
@@ -258,11 +400,27 @@ class KomgaClient(RestApiMixin):
return False, "User or e-mail already exists."
try:
user_id = self.create_user(username, password, email)
inv = Invitation.query.filter_by(code=code).first()
current_server_id = getattr(self, "server_id", None)
# Get download permission from invitation, or fall back to server default
if inv and inv.allow_downloads is not None:
allow_downloads = inv.allow_downloads
else:
# Use server's default download policy
from app.models import MediaServer
server = (
MediaServer.query.get(current_server_id)
if current_server_id
else None
)
allow_downloads = server.allow_downloads if server else False
user_id = self.create_user(
username, password, email, allow_downloads=allow_downloads
)
if inv and inv.libraries:
library_ids = [
lib.external_id
@@ -339,7 +497,7 @@ class KomgaClient(RestApiMixin):
# User statistics - only what's displayed in UI
try:
users_response = self.get("/api/v1/users").json()
users_response = self.get("/api/v2/users").json()
stats["user_stats"] = {
"total_users": len(users_response),
"active_sessions": 0, # Komga doesn't have active sessions concept
@@ -389,7 +547,7 @@ class KomgaClient(RestApiMixin):
else:
# Ultimate fallback: API call
try:
users = self.get("/api/v1/users").json()
users = self.get("/api/v2/users").json()
count = len(users) if isinstance(users, list) else 0
except Exception as api_error:
logging.warning(f"Komga API fallback failed: {api_error}")
@@ -449,3 +607,45 @@ class KomgaClient(RestApiMixin):
"content_stats": {},
"error": str(e),
}
def get_recent_items(self, limit: int = 6) -> list[dict[str, str]]:
"""Get recently added books from Komga for the wizard widget.
Args:
limit: Maximum number of items to return
Returns:
list: List of dicts with 'title' and 'thumb' keys
"""
try:
# Get latest books from Komga API
response = self.get(f"/api/v1/books/latest?size={limit}")
books = response.json().get("content", [])
items = []
for book in books:
# Get book ID for thumbnail
book_id = book.get("id")
if book_id:
# Construct thumbnail URL with authentication
thumb_url = (
f"{self.url.rstrip('/')}/api/v1/books/{book_id}/thumbnail"
)
# Generate secure proxy URL with opaque token
thumb_url = self.generate_image_proxy_url(thumb_url)
items.append(
{
"title": book.get("metadata", {}).get("title")
or book.get("name", "Unknown"),
"thumb": thumb_url,
}
)
return items
except Exception as e:
logging.warning(f"Failed to get recent items from Komga: {e}")
return []

View File

@@ -148,12 +148,12 @@ def check_komga(url: str, token: str) -> tuple[bool, str]:
We perform a lightweight GET request to ``/api/v1/libraries`` which is
available to authenticated users and returns a list of libraries in
JSON. When *token* is set we send it as a *Bearer* header.
JSON. When *token* is set we send it as an *X-API-Key* header.
"""
try:
headers = {"Accept": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
headers["X-API-Key"] = token
resp = requests.get(
f"{url.rstrip('/')}/api/v1/libraries", headers=headers, timeout=10

View File

@@ -112,22 +112,22 @@
function closeModal(){ document.getElementById('create-server-modal').innerHTML=''; }
function updateOptions(){
const type=document.getElementById('server_type').value;
// Show universal media server options for Plex, Jellyfin, Emby, and Audiobookshelf
const showMediaOptions = ['plex', 'jellyfin', 'emby', 'audiobookshelf'].includes(type);
// Show universal media server options for Plex, Jellyfin, Emby, Audiobookshelf, and Komga
const showMediaOptions = ['plex', 'jellyfin', 'emby', 'audiobookshelf', 'komga'].includes(type);
document.getElementById('media-server-options').style.display = showMediaOptions ? 'block' : 'none';
// Hide "Allow Live TV" for Audiobookshelf since it doesn't support Live TV
// Hide "Allow Live TV" for Audiobookshelf and Komga since they don't support Live TV
const allowLiveTvOption = document.getElementById('allow-live-tv-option');
if (allowLiveTvOption) {
allowLiveTvOption.style.display = type === 'audiobookshelf' ? 'none' : 'block';
allowLiveTvOption.style.display = ['audiobookshelf', 'komga'].includes(type) ? 'none' : 'block';
}
// Hide "Allow Mobile Uploads" for Audiobookshelf and Jellyfin since they don't support mobile uploads
// Hide "Allow Mobile Uploads" for Audiobookshelf, Jellyfin, and Komga since they don't support mobile uploads
const allowMobileUploadsOption = document.getElementById('allow-mobile-uploads-option');
if (allowMobileUploadsOption) {
allowMobileUploadsOption.style.display = ['plex', 'emby'].includes(type) ? 'block' : 'none';
}
document.getElementById('romm-options').style.display = type==='romm' ? 'block' : 'none';
document.getElementById('api-key-div').style.display = type==='romm' ? 'none' : 'block';
}

View File

@@ -114,22 +114,22 @@
function closeModal(){ document.getElementById('create-server-modal').innerHTML=''; }
function updateOptions(){
const type=document.getElementById('server_type').value;
// Show universal media server options for Plex, Jellyfin, Emby, and Audiobookshelf
const showMediaOptions = ['plex', 'jellyfin', 'emby', 'audiobookshelf'].includes(type);
// Show universal media server options for Plex, Jellyfin, Emby, Audiobookshelf, and Komga
const showMediaOptions = ['plex', 'jellyfin', 'emby', 'audiobookshelf', 'komga'].includes(type);
document.getElementById('media-server-options').style.display = showMediaOptions ? 'block' : 'none';
// Hide "Allow Live TV" for Audiobookshelf since it doesn't support Live TV
// Hide "Allow Live TV" for Audiobookshelf and Komga since they don't support Live TV
const allowLiveTvOption = document.getElementById('allow-live-tv-option-edit');
if (allowLiveTvOption) {
allowLiveTvOption.style.display = type === 'audiobookshelf' ? 'none' : 'block';
allowLiveTvOption.style.display = ['audiobookshelf', 'komga'].includes(type) ? 'none' : 'block';
}
// Hide "Allow Mobile Uploads" for Audiobookshelf and Jellyfin since they don't support mobile uploads
// Hide "Allow Mobile Uploads" for Audiobookshelf, Jellyfin, and Komga since they don't support mobile uploads
const allowMobileUploadsOption = document.getElementById('allow-mobile-uploads-option-edit');
if (allowMobileUploadsOption) {
allowMobileUploadsOption.style.display = ['plex', 'emby'].includes(type) ? 'block' : 'none';
}
document.getElementById('romm-options').style.display = type==='romm' ? 'block' : 'none';
document.getElementById('api-key-div').style.display = type==='romm' ? 'none' : 'block';
}

View File

@@ -237,6 +237,12 @@
<div class="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary dark:peer-focus:ring-primary rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary dark:peer-checked:bg-primary"></div>
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300">{{ _("Allow Downloads") }}</span>
</label>
{% elif s.server_type == "komga" %}
<label class="flex items-center cursor-pointer">
<input id="allow_downloads_{{ s.id }}" name="allow_downloads" type="checkbox" value="true" {% if s.allow_downloads %}checked{% endif %} class="sr-only peer">
<div class="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary dark:peer-focus:ring-primary rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary dark:peer-checked:bg-primary"></div>
<span class="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300">{{ _("Allow Downloads") }}</span>
</label>
{% endif %}
<!-- Library dropdown-search -->

View File

@@ -1,27 +1,23 @@
---
title: "Welcome to Komga!"
title: "{{ _('What is Komga?') }}"
---
# 📚 {{ _("Welcome to Your Digital Library") }}
## 📚 {{ _('Welcome to Our Komga Server') }}
{{ _("You've been invited to access our Komga comic and manga library! "
"Komga is a digital library server that organizes and serves your comics, "
"manga, and digital books.") }}
{{ _('Great news — you now have access to our **digital comic and manga library** through Komga!') }}
{{ widget:recently_added_media }}
## 🎯 {{ _("What can you do?") }}
|||
### 📖 {{ _('What is Komga?') }}
- **Browse Collections**: {{ _("Explore organized series and collections") }}
- **Read Online**: {{ _("Read directly in your web browser") }}
- **Download**: {{ _("Download comics for offline reading") }}
- **Track Progress**: {{ _("Keep track of what you've read") }}
- **Multiple Formats**: {{ _("Support for CBZ, CBR, PDF, and more") }}
{{ _('Komga is a free, open-source media server designed specifically for comics, manga, and digital books. If you\'re here, it means you\'ve been invited to join our library — welcome!') }}
|||
## 🚀 {{ _("Getting Started") }}
{{ _("You can access Komga through:") }}
1. **Web Interface**: {{ _("Use any web browser to access your library") }}
2. **Mobile Apps**: {{ _("Download compatible comic reader apps") }}
3. **Desktop Apps**: {{ _("Use desktop comic readers that support OPDS") }}
{{ _("Let's get you set up!") }}
|||
### 🎯 {{ _('What You\'ll Get') }}
- {{ _('Access to our constantly updated collection of comics and manga') }}
- {{ _('Read online directly in your browser') }}
- {{ _('Download for offline reading on your favorite devices') }}
- {{ _('Track your reading progress automatically') }}
- {{ _('Support for multiple formats (CBZ, CBR, PDF, EPUB)') }}
|||

View File

@@ -1,37 +1,21 @@
---
title: "How to Access Your Comics"
title: "{{ _('Download Komga Clients') }}"
---
# 💻 {{ _("Accessing Your Library") }}
## 💾 {{ _('Download Komga Clients') }}
## 🌐 {{ _("Web Interface") }}
{{ _('Komga has many compatible readers from which you can enjoy your comics and manga!') }}
{{ _("The easiest way to start reading is through your web browser:") }}
|||
### 📱 {{ _('Read Anywhere') }}
1. **Visit**: {{ server_url }}
2. **Login**: {{ _("Use the credentials you just created") }}
3. **Browse**: {{ _("Explore your available comics and series") }}
4. **Read**: {{ _("Click any comic to start reading online") }}
{{ _('Komga works with many popular comic readers — install apps on your favorite devices:') }}
[{{ _("Open Komga Web Interface") }}]({{ server_url }}){:target="_blank" .btn}
- 📱 **{{ _('Mobile') }}**: {{ _('iOS (Panels, Chunky) & Android (Mihon/Tachiyomi, Komga Client)') }}
- 🖥️ **{{ _('Desktop') }}**: {{ _('Windows (CDisplayEx, YACReader), macOS & Linux (YACReader)') }}
- 📺 **{{ _('Tablets') }}**: {{ _('iPad & Android tablets with comic reader apps') }}
- 🌐 **{{ _('Web App') }}**: {{ _('Read instantly in your browser at') }} {{ server_url }}
- 📖 **{{ _('E-Readers') }}**: {{ _('KOReader and other OPDS-compatible readers') }}
|||
## 📱 {{ _("Mobile Apps") }}
{{ _("For the best mobile reading experience:") }}
### {{ _("Android") }}
- **Mihon (Tachiyomi)**: {{ _("Popular manga reader with Komga support") }}
- **Komga Client**: {{ _("Official Komga mobile app") }}
- **Moon+ Reader**: {{ _("Support for OPDS feeds") }}
### {{ _("iOS") }}
- **Panels**: {{ _("Premium comic reader with Komga integration") }}
- **Chunky Comic Reader**: {{ _("Supports OPDS feeds") }}
## 🖥️ {{ _("Desktop Apps") }}
- **YACReader**: {{ _("Cross-platform comic reader") }}
- **CDisplayEx**: {{ _("Windows comic reader") }}
- **Komga Web**: {{ _("Use the web interface in full-screen mode") }}
{{ _("Most apps support OPDS feeds - ask your admin for the OPDS URL if needed.") }}
{{ widget:button url="https://komga.org/guides/apps.html" text=_("📚 View Compatible Readers") }}

View File

@@ -1,42 +1,34 @@
---
title: "Library Features"
title: "{{ _('Library Features') }}"
---
# ✨ {{ _("Library Features") }}
## ✨ {{ _('Komga Features') }}
## 📖 {{ _("Reading Experience") }}
{{ _('Discover all the features available to enhance your reading experience!') }}
- **Webtoon Mode**: {{ _("Perfect for manga and webtoons") }}
- **Page-by-Page**: {{ _("Traditional comic reading") }}
- **Zoom & Pan**: {{ _("Detailed view of artwork") }}
- **Bookmarks**: {{ _("Save your progress automatically") }}
|||
### 📖 {{ _('Reading Experience') }}
## 🗂️ {{ _("Organization") }}
- **{{ _('Webtoon Mode') }}**: {{ _('Perfect for manga and webtoons with continuous scrolling') }}
- **{{ _('Page-by-Page') }}**: {{ _('Traditional comic book reading experience') }}
- **{{ _('Zoom & Pan') }}**: {{ _('Get detailed views of artwork') }}
- **{{ _('Auto Bookmarks') }}**: {{ _('Your progress is saved automatically') }}
|||
- **Series**: {{ _("Comics grouped by series") }}
- **Collections**: {{ _("Curated collections by themes") }}
- **Tags**: {{ _("Filter by genres, authors, and more") }}
- **Search**: {{ _("Find specific titles quickly") }}
|||
### 🗂️ {{ _('Organization') }}
## 👤 {{ _("Personal Features") }}
- **{{ _('Series Collections') }}**: {{ _('Browse comics organized by series') }}
- **{{ _('Custom Collections') }}**: {{ _('Curated collections by themes and genres') }}
- **{{ _('Smart Tags') }}**: {{ _('Filter by genres, authors, publishers, and more') }}
- **{{ _('Advanced Search') }}**: {{ _('Find specific titles instantly') }}
|||
- **Reading Progress**: {{ _("Track which issues you've read") }}
- **Continue Reading**: {{ _("Pick up where you left off") }}
- **Reading Lists**: {{ _("Create custom reading lists") }}
- **Favorites**: {{ _("Mark your favorite series") }}
|||
### 👤 {{ _('Personal Features') }}
## 🔍 {{ _("Discovery") }}
- **Latest Releases**: {{ _("See newly added comics") }}
- **Recently Updated**: {{ _("Series with new issues") }}
- **Recommendations**: {{ _("Based on your reading history") }}
## 💾 {{ _("Offline Reading") }}
{{ _("Many compatible apps allow you to:") }}
- **Download**: {{ _("Save comics locally") }}
- **Sync**: {{ _("Keep progress synchronized") }}
- **Offline Mode**: {{ _("Read without internet") }}
{{ _("Your reading progress syncs back when you reconnect!") }}
- **{{ _('Reading Progress') }}**: {{ _('Track which issues you\'ve completed') }}
- **{{ _('Continue Reading') }}**: {{ _('Pick up exactly where you left off') }}
- **{{ _('Reading Lists') }}**: {{ _('Create and manage custom reading lists') }}
- **{{ _('On Deck') }}**: {{ _('See your next suggested reads') }}
|||

View File

@@ -1,45 +0,0 @@
---
title: "Tips & Tricks"
---
# 💡 {{ _("Tips for the Best Experience") }}
## 🎯 {{ _("Reading Tips") }}
- **Reading Direction**: {{ _("Check if manga should be read right-to-left") }}
- **Display Settings**: {{ _("Adjust brightness and contrast for comfort") }}
- **Full Screen**: {{ _("Use full-screen mode for immersive reading") }}
- **Keyboard Shortcuts**: {{ _("Learn shortcuts for faster navigation") }}
## 🔧 {{ _("Browser Settings") }}
- **Zoom Level**: {{ _("Adjust browser zoom for optimal text size") }}
- **Dark Mode**: {{ _("Enable dark mode for night reading") }}
- **Notifications**: {{ _("Allow notifications for new releases") }}
## 📱 {{ _("Mobile Reading") }}
- **Landscape Mode**: {{ _("Some comics work better in landscape") }}
- **Gesture Controls**: {{ _("Learn swipe gestures for navigation") }}
- **Auto-Brightness**: {{ _("Adjust screen brightness automatically") }}
## 🎨 {{ _("Customization") }}
- **Theme**: {{ _("Choose light or dark theme") }}
- **Reader Settings**: {{ _("Adjust page fit and reading direction") }}
- **Layout**: {{ _("Customize library view (grid, list, etc.)") }}
## 🔍 {{ _("Organization Tips") }}
- **Use Search**: {{ _("Search supports title, author, and tags") }}
- **Filter by Status**: {{ _("Find ongoing vs completed series") }}
- **Sort Options**: {{ _("Sort by added date, name, or progress") }}
- **Mark as Read**: {{ _("Keep track of your reading progress") }}
## 🚀 {{ _("Advanced Features") }}
- **OPDS Feed**: {{ _("Use OPDS URL in compatible apps") }}
- **API Access**: {{ _("For developers - full REST API available") }}
- **Metadata**: {{ _("Rich metadata for better organization") }}
{{ _("Happy reading! 📚✨") }}

View File

@@ -0,0 +1,17 @@
---
title: "{{ _('Tips for the best experience') }}"
---
## 📚 {{ _('Get the best reading experience') }}
{{ _('Komga offers a great reading experience, but here\'s how to make it even better:') }}
1. **{{ _('Choose the right reader mode') }}** {{ _('Use webtoon mode for manga, page-by-page for traditional comics') }}
2. **{{ _('Adjust reading direction') }}** {{ _('Settings → Reader → set right-to-left for manga') }}
3. **{{ _('Enable dark mode') }}** {{ _('Easier on your eyes during night reading sessions') }}
4. **{{ _('Use full-screen mode') }}** {{ _('Press F11 in browser for distraction-free reading') }}
5. **{{ _('Download for offline') }}** {{ _('Download comics to mobile apps for reading without internet') }}
*{{ _('Happy reading!') }}* 📖
{{ widget:button url="external_url" text=_("Go to Komga") }}