mirror of
https://github.com/miniflux/v2.git
synced 2025-06-27 16:36:00 +00:00
Merge d88b8a1d3b
into b583de88f3
This commit is contained in:
commit
2729a76ce4
17 changed files with 2555 additions and 1679 deletions
|
@ -46,6 +46,7 @@ type User struct {
|
|||
BlockFilterEntryRules string `json:"block_filter_entry_rules"`
|
||||
KeepFilterEntryRules string `json:"keep_filter_entry_rules"`
|
||||
ExternalFontHosts string `json:"external_font_hosts"`
|
||||
CacheForOffline bool `json:"cache_for_offline"`
|
||||
}
|
||||
|
||||
func (u User) String() string {
|
||||
|
|
|
@ -970,6 +970,7 @@ var migrations = []func(tx *sql.Tx, driver string) error{
|
|||
return err
|
||||
},
|
||||
func(tx *sql.Tx, _ string) (err error) {
|
||||
<<<<<<< HEAD
|
||||
sql := `
|
||||
ALTER TABLE integrations ADD COLUMN discord_enabled bool default 'f';
|
||||
ALTER TABLE integrations ADD COLUMN discord_webhook_link text default '';
|
||||
|
@ -1015,4 +1016,9 @@ var migrations = []func(tx *sql.Tx, driver string) error{
|
|||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
func(tx *sql.Tx, _ string) (err error) {
|
||||
sql := `ALTER TABLE users ADD COLUMN cache_for_offline boolean default 'f'`
|
||||
_, err = tx.Exec(sql)
|
||||
return err
|
||||
},
|
||||
}
|
||||
|
|
|
@ -140,6 +140,11 @@ func LastForceRefresh(r *http.Request) int64 {
|
|||
return timestamp
|
||||
}
|
||||
|
||||
// Determine if the request is from a service worker.
|
||||
func IsServiceWorker(r *http.Request) bool {
|
||||
return r.Header.Get("Client-Type") == "service-worker"
|
||||
}
|
||||
|
||||
// ClientIP returns the client IP address stored in the context.
|
||||
func ClientIP(r *http.Request) string {
|
||||
return getContextStringValue(r, ClientIPContextKey)
|
||||
|
|
|
@ -100,7 +100,7 @@ func (b *Builder) Write() {
|
|||
func (b *Builder) writeHeaders() {
|
||||
b.headers["X-Content-Type-Options"] = "nosniff"
|
||||
b.headers["X-Frame-Options"] = "DENY"
|
||||
b.headers["Referrer-Policy"] = "no-referrer"
|
||||
b.headers["Referrer-Policy"] = "strict-origin"
|
||||
|
||||
for key, value := range b.headers {
|
||||
b.w.Header().Set(key, value)
|
||||
|
|
|
@ -53,10 +53,7 @@
|
|||
"entry.bookmark.toggle.on": "Star",
|
||||
"entry.comments.label": "Comments",
|
||||
"entry.comments.title": "View Comments",
|
||||
"entry.estimated_reading_time": [
|
||||
"%d minute read",
|
||||
"%d minutes read"
|
||||
],
|
||||
"entry.estimated_reading_time": ["%d minute read", "%d minutes read"],
|
||||
"entry.external_link.label": "External link",
|
||||
"entry.save.completed": "Done!",
|
||||
"entry.save.label": "Save",
|
||||
|
@ -421,15 +418,9 @@
|
|||
"page.api_keys.table.last_used_at": "Last Used",
|
||||
"page.api_keys.table.token": "Token",
|
||||
"page.api_keys.title": "API Keys",
|
||||
"page.categories_count": [
|
||||
"%d category",
|
||||
"%d categories"
|
||||
],
|
||||
"page.categories_count": ["%d category", "%d categories"],
|
||||
"page.categories.entries": "Entries",
|
||||
"page.categories.feed_count": [
|
||||
"There is %d feed.",
|
||||
"There are %d feeds."
|
||||
],
|
||||
"page.categories.feed_count": ["There is %d feed.", "There are %d feeds."],
|
||||
"page.categories.feeds": "Feeds",
|
||||
"page.categories.no_feed": "No feed.",
|
||||
"page.categories.title": "Categories",
|
||||
|
@ -443,10 +434,7 @@
|
|||
"page.edit_feed.title": "Edit Feed: %s",
|
||||
"page.edit_user.title": "Edit User: %s",
|
||||
"page.entry.attachments": "Attachments",
|
||||
"page.feeds.error_count": [
|
||||
"%d error",
|
||||
"%d errors"
|
||||
],
|
||||
"page.feeds.error_count": ["%d error", "%d errors"],
|
||||
"page.feeds.last_check": "Last check:",
|
||||
"page.feeds.next_check": "Next check:",
|
||||
"page.feeds.read_counter": "Number of read entries",
|
||||
|
@ -511,10 +499,7 @@
|
|||
"page.offline.message": "You are offline",
|
||||
"page.offline.refresh_page": "Try to refresh the page",
|
||||
"page.offline.title": "Offline Mode",
|
||||
"page.read_entry_count": [
|
||||
"%d read entry",
|
||||
"%d read entries"
|
||||
],
|
||||
"page.read_entry_count": ["%d read entry", "%d read entries"],
|
||||
"page.search.title": "Search Results",
|
||||
"page.sessions.table.actions": "Actions",
|
||||
"page.sessions.table.current_session": "Current Session",
|
||||
|
@ -529,33 +514,18 @@
|
|||
"page.settings.unlink_oidc_account": "Unlink my %s account",
|
||||
"page.settings.webauthn.actions": "Actions",
|
||||
"page.settings.webauthn.added_on": "Added On",
|
||||
"page.settings.webauthn.delete": [
|
||||
"Remove %d passkey",
|
||||
"Remove %d passkeys"
|
||||
],
|
||||
"page.settings.webauthn.delete": ["Remove %d passkey", "Remove %d passkeys"],
|
||||
"page.settings.webauthn.last_seen_on": "Last Used",
|
||||
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||
"page.settings.webauthn.passkeys": "Passkeys",
|
||||
"page.settings.webauthn.register": "Register passkey",
|
||||
"page.settings.webauthn.register.error": "Unable to register passkey",
|
||||
"page.shared_entries_count": [
|
||||
"%d shared entry",
|
||||
"%d shared entries"
|
||||
],
|
||||
"page.shared_entries_count": ["%d shared entry", "%d shared entries"],
|
||||
"page.shared_entries.title": "Shared entries",
|
||||
"page.starred_entry_count": [
|
||||
"%d starred entry",
|
||||
"%d starred entries"
|
||||
],
|
||||
"page.starred_entry_count": ["%d starred entry", "%d starred entries"],
|
||||
"page.starred.title": "Starred",
|
||||
"page.total_entry_count": [
|
||||
"%d entry in total",
|
||||
"%d entries in total"
|
||||
],
|
||||
"page.unread_entry_count": [
|
||||
"%d unread entry",
|
||||
"%d unread entries"
|
||||
],
|
||||
"page.total_entry_count": ["%d entry in total", "%d entries in total"],
|
||||
"page.unread_entry_count": ["%d unread entry", "%d unread entries"],
|
||||
"page.unread.title": "Unread",
|
||||
"page.users.actions": "Actions",
|
||||
"page.users.admin.no": "No",
|
||||
|
@ -574,33 +544,553 @@
|
|||
"search.placeholder": "Search…",
|
||||
"search.submit": "Search",
|
||||
"skip_to_content": "Skip to content",
|
||||
"time_elapsed.days": [
|
||||
"%d day ago",
|
||||
"%d days ago"
|
||||
],
|
||||
"time_elapsed.hours": [
|
||||
"%d hour ago",
|
||||
"%d hours ago"
|
||||
],
|
||||
"time_elapsed.minutes": [
|
||||
"%d minute ago",
|
||||
"%d minutes ago"
|
||||
],
|
||||
"time_elapsed.months": [
|
||||
"%d month ago",
|
||||
"%d months ago"
|
||||
],
|
||||
"time_elapsed.days": ["%d day ago", "%d days ago"],
|
||||
"time_elapsed.hours": ["%d hour ago", "%d hours ago"],
|
||||
"time_elapsed.minutes": ["%d minute ago", "%d minutes ago"],
|
||||
"time_elapsed.months": ["%d month ago", "%d months ago"],
|
||||
"time_elapsed.not_yet": "not yet",
|
||||
"time_elapsed.now": "just now",
|
||||
"time_elapsed.weeks": [
|
||||
"%d week ago",
|
||||
"%d weeks ago"
|
||||
],
|
||||
"time_elapsed.years": [
|
||||
"%d year ago",
|
||||
"%d years ago"
|
||||
],
|
||||
"time_elapsed.weeks": ["%d week ago", "%d weeks ago"],
|
||||
"time_elapsed.years": ["%d year ago", "%d years ago"],
|
||||
"time_elapsed.yesterday": "yesterday",
|
||||
"tooltip.keyboard_shortcuts": "Keyboard Shortcut: %s",
|
||||
"tooltip.logged_user": "Logged in as %s"
|
||||
"tooltip.logged_user": "Logged in as %s",
|
||||
"skip_to_content": "Skip to content",
|
||||
"confirm.question": "Are you sure?",
|
||||
"confirm.question.refresh": "Are you sure you want to force refresh?",
|
||||
"confirm.yes": "yes",
|
||||
"confirm.no": "no",
|
||||
"confirm.loading": "In progress…",
|
||||
"action.subscribe": "Subscribe",
|
||||
"action.save": "Save",
|
||||
"action.or": "or",
|
||||
"action.cancel": "cancel",
|
||||
"action.remove": "Remove",
|
||||
"action.remove_feed": "Remove this feed",
|
||||
"action.update": "Update",
|
||||
"action.edit": "Edit",
|
||||
"action.download": "Download",
|
||||
"action.import": "Import",
|
||||
"action.login": "Login",
|
||||
"action.home_screen": "Add to home screen",
|
||||
"tooltip.keyboard_shortcuts": "Keyboard Shortcut: %s",
|
||||
"tooltip.logged_user": "Logged in as %s",
|
||||
"menu.title": "Menu",
|
||||
"menu.home_page": "Home page",
|
||||
"menu.unread": "Unread",
|
||||
"menu.starred": "Starred",
|
||||
"menu.history": "History",
|
||||
"menu.feeds": "Feeds",
|
||||
"menu.categories": "Categories",
|
||||
"menu.settings": "Settings",
|
||||
"menu.logout": "Logout",
|
||||
"menu.preferences": "Preferences",
|
||||
"menu.integrations": "Integrations",
|
||||
"menu.sessions": "Sessions",
|
||||
"menu.users": "Users",
|
||||
"menu.about": "About",
|
||||
"menu.export": "Export",
|
||||
"menu.import": "Import",
|
||||
"menu.search": "Search",
|
||||
"menu.create_category": "Create a category",
|
||||
"menu.mark_page_as_read": "Mark this page as read",
|
||||
"menu.mark_all_as_read": "Mark all as read",
|
||||
"menu.show_all_entries": "Show all entries",
|
||||
"menu.show_only_starred_entries": "Show only starred entries",
|
||||
"menu.show_only_unread_entries": "Show only unread entries",
|
||||
"menu.refresh_feed": "Refresh",
|
||||
"menu.refresh_all_feeds": "Refresh all feeds in the background",
|
||||
"menu.edit_feed": "Edit",
|
||||
"menu.edit_category": "Edit",
|
||||
"menu.add_feed": "Add feed",
|
||||
"menu.add_user": "Add user",
|
||||
"menu.flush_history": "Flush history",
|
||||
"menu.feed_entries": "Entries",
|
||||
"menu.api_keys": "API Keys",
|
||||
"menu.create_api_key": "Create a new API key",
|
||||
"menu.shared_entries": "Shared entries",
|
||||
"search.label": "Search",
|
||||
"search.placeholder": "Search…",
|
||||
"search.submit": "Search",
|
||||
"pagination.last": "Last",
|
||||
"pagination.next": "Next",
|
||||
"pagination.first": "First",
|
||||
"pagination.previous": "Previous",
|
||||
"entry.status.unread": "Unread",
|
||||
"entry.status.read": "Read",
|
||||
"entry.status.toast.unread": "Marked as unread",
|
||||
"entry.status.toast.read": "Marked as read",
|
||||
"entry.status.title": "Change entry status",
|
||||
"entry.bookmark.toggle.on": "Star",
|
||||
"entry.bookmark.toggle.off": "Unstar",
|
||||
"entry.bookmark.toast.on": "Starred",
|
||||
"entry.bookmark.toast.off": "Unstarred",
|
||||
"entry.state.saving": "Saving…",
|
||||
"entry.state.loading": "Loading…",
|
||||
"entry.save.label": "Save",
|
||||
"entry.save.title": "Save this entry",
|
||||
"entry.save.completed": "Done!",
|
||||
"entry.save.toast.completed": "Entry saved",
|
||||
"entry.scraper.label": "Download",
|
||||
"entry.scraper.title": "Fetch original content",
|
||||
"entry.scraper.completed": "Done!",
|
||||
"entry.external_link.label": "External link",
|
||||
"entry.comments.label": "Comments",
|
||||
"entry.comments.title": "View Comments",
|
||||
"entry.share.label": "Share",
|
||||
"entry.share.title": "Share this entry",
|
||||
"entry.unshare.label": "Unshare",
|
||||
"entry.shared_entry.title": "Open the public link",
|
||||
"entry.shared_entry.label": "Share",
|
||||
"entry.estimated_reading_time": ["%d minute read", "%d minutes read"],
|
||||
"entry.tags.label": "Tags:",
|
||||
"page.shared_entries.title": "Shared entries",
|
||||
"page.shared_entries_count": ["%d shared entry", "%d shared entries"],
|
||||
"page.unread.title": "Unread",
|
||||
"page.unread_entry_count": ["%d unread entry", "%d unread entries"],
|
||||
"page.total_entry_count": ["%d entry in total", "%d entries in total"],
|
||||
"page.starred.title": "Starred",
|
||||
"page.starred_entry_count": ["%d starred entry", "%d starred entries"],
|
||||
"page.categories.title": "Categories",
|
||||
"page.categories.no_feed": "No feed.",
|
||||
"page.categories.entries": "Entries",
|
||||
"page.categories.feeds": "Feeds",
|
||||
"page.categories.feed_count": ["There is %d feed.", "There are %d feeds."],
|
||||
"page.categories_count": ["%d category", "%d categories"],
|
||||
"page.new_category.title": "New Category",
|
||||
"page.new_user.title": "New User",
|
||||
"page.edit_category.title": "Edit Category: %s",
|
||||
"page.edit_user.title": "Edit User: %s",
|
||||
"page.feeds.title": "Feeds",
|
||||
"page.category_label": "Category: %s",
|
||||
"page.feeds.last_check": "Last check:",
|
||||
"page.feeds.next_check": "Next check:",
|
||||
"page.feeds.read_counter": "Number of read entries",
|
||||
"page.feeds.error_count": ["%d error", "%d errors"],
|
||||
"page.history.title": "History",
|
||||
"page.read_entry_count": ["%d read entry", "%d read entries"],
|
||||
"page.import.title": "Import",
|
||||
"page.search.title": "Search Results",
|
||||
"page.about.title": "About",
|
||||
"page.about.credits": "Credits",
|
||||
"page.about.version": "Version:",
|
||||
"page.about.build_date": "Build Date:",
|
||||
"page.about.author": "Author:",
|
||||
"page.about.license": "License:",
|
||||
"page.about.global_config_options": "Global configuration options",
|
||||
"page.about.postgres_version": "Postgres version:",
|
||||
"page.about.go_version": "Go version:",
|
||||
"page.add_feed.title": "New feed",
|
||||
"page.add_feed.no_category": "There is no category. You must have at least one category.",
|
||||
"page.add_feed.label.url": "URL",
|
||||
"page.add_feed.submit": "Find a feed",
|
||||
"page.add_feed.legend.advanced_options": "Advanced Options",
|
||||
"page.add_feed.choose_feed": "Choose a feed",
|
||||
"page.edit_feed.title": "Edit Feed: %s",
|
||||
"page.edit_feed.last_check": "Last check:",
|
||||
"page.edit_feed.last_modified_header": "LastModified header:",
|
||||
"page.edit_feed.etag_header": "ETag header:",
|
||||
"page.edit_feed.no_header": "None",
|
||||
"page.edit_feed.last_parsing_error": "Last Parsing Error",
|
||||
"page.entry.attachments": "Attachments",
|
||||
"page.keyboard_shortcuts.title": "Keyboard Shortcuts",
|
||||
"page.keyboard_shortcuts.subtitle.sections": "Sections Navigation",
|
||||
"page.keyboard_shortcuts.subtitle.items": "Items Navigation",
|
||||
"page.keyboard_shortcuts.subtitle.pages": "Pages Navigation",
|
||||
"page.keyboard_shortcuts.subtitle.actions": "Actions",
|
||||
"page.keyboard_shortcuts.go_to_unread": "Go to unread",
|
||||
"page.keyboard_shortcuts.go_to_starred": "Go to starred",
|
||||
"page.keyboard_shortcuts.go_to_history": "Go to history",
|
||||
"page.keyboard_shortcuts.go_to_feeds": "Go to feeds",
|
||||
"page.keyboard_shortcuts.go_to_categories": "Go to categories",
|
||||
"page.keyboard_shortcuts.go_to_settings": "Go to settings",
|
||||
"page.keyboard_shortcuts.show_keyboard_shortcuts": "Show keyboard shortcuts",
|
||||
"page.keyboard_shortcuts.go_to_previous_item": "Go to previous item",
|
||||
"page.keyboard_shortcuts.go_to_next_item": "Go to next item",
|
||||
"page.keyboard_shortcuts.go_to_feed": "Go to feed",
|
||||
"page.keyboard_shortcuts.go_to_top_item": "Go to top item",
|
||||
"page.keyboard_shortcuts.go_to_bottom_item": "Go to bottom item",
|
||||
"page.keyboard_shortcuts.go_to_previous_page": "Go to previous page",
|
||||
"page.keyboard_shortcuts.go_to_next_page": "Go to next page",
|
||||
"page.keyboard_shortcuts.open_item": "Open selected item",
|
||||
"page.keyboard_shortcuts.open_original": "Open original link",
|
||||
"page.keyboard_shortcuts.open_original_same_window": "Open original link in current tab",
|
||||
"page.keyboard_shortcuts.open_comments": "Open comments link",
|
||||
"page.keyboard_shortcuts.open_comments_same_window": "Open comments link in current tab",
|
||||
"page.keyboard_shortcuts.toggle_read_status_next": "Toggle read/unread, focus next",
|
||||
"page.keyboard_shortcuts.toggle_read_status_prev": "Toggle read/unread, focus previous",
|
||||
"page.keyboard_shortcuts.refresh_all_feeds": "Refresh all feeds in the background",
|
||||
"page.keyboard_shortcuts.mark_page_as_read": "Mark current page as read",
|
||||
"page.keyboard_shortcuts.download_content": "Download original content",
|
||||
"page.keyboard_shortcuts.toggle_bookmark_status": "Toggle starred",
|
||||
"page.keyboard_shortcuts.save_article": "Save entry",
|
||||
"page.keyboard_shortcuts.scroll_item_to_top": "Scroll item to top",
|
||||
"page.keyboard_shortcuts.remove_feed": "Remove this feed",
|
||||
"page.keyboard_shortcuts.go_to_search": "Set focus on search form",
|
||||
"page.keyboard_shortcuts.toggle_entry_attachments": "Toggle open/close entry attachments",
|
||||
"page.keyboard_shortcuts.close_modal": "Close modal dialog",
|
||||
"page.users.title": "Users",
|
||||
"page.users.username": "Username",
|
||||
"page.users.never_logged": "Never",
|
||||
"page.users.admin.yes": "Yes",
|
||||
"page.users.admin.no": "No",
|
||||
"page.users.actions": "Actions",
|
||||
"page.users.last_login": "Last Login",
|
||||
"page.users.is_admin": "Administrator",
|
||||
"page.settings.title": "Settings",
|
||||
"page.settings.link_google_account": "Link my Google account",
|
||||
"page.settings.unlink_google_account": "Unlink my Google account",
|
||||
"page.settings.link_oidc_account": "Link my %s account",
|
||||
"page.settings.unlink_oidc_account": "Unlink my %s account",
|
||||
"page.settings.webauthn.passkeys": "Passkeys",
|
||||
"page.settings.webauthn.actions": "Actions",
|
||||
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||
"page.settings.webauthn.added_on": "Added On",
|
||||
"page.settings.webauthn.last_seen_on": "Last Used",
|
||||
"page.settings.webauthn.register": "Register passkey",
|
||||
"page.settings.webauthn.register.error": "Unable to register passkey",
|
||||
"page.settings.webauthn.delete": ["Remove %d passkey", "Remove %d passkeys"],
|
||||
"page.login.title": "Sign In",
|
||||
"page.login.google_signin": "Sign in with Google",
|
||||
"page.login.oidc_signin": "Sign in with %s",
|
||||
"page.login.webauthn_login": "Login with passkey",
|
||||
"page.login.webauthn_login.error": "Unable to login with passkey",
|
||||
"page.login.webauthn_login.help": "Please enter your username if you're using a security key. This is not required if you are using a Passkey (discoverable credentials).",
|
||||
"page.integrations.title": "Integrations",
|
||||
"page.integration.miniflux_api": "Miniflux API",
|
||||
"page.integration.miniflux_api_endpoint": "API Endpoint",
|
||||
"page.integration.miniflux_api_username": "Username",
|
||||
"page.integration.miniflux_api_password": "Password",
|
||||
"page.integration.miniflux_api_password_value": "Your account password",
|
||||
"page.integration.bookmarklet": "Bookmarklet",
|
||||
"page.integration.bookmarklet.name": "Add to Miniflux",
|
||||
"page.integration.bookmarklet.instructions": "Drag and drop this link to your bookmarks.",
|
||||
"page.integration.bookmarklet.help": "This special link allows you to subscribe to a website directly by using a bookmark in your web browser.",
|
||||
"page.sessions.title": "Sessions",
|
||||
"page.sessions.table.date": "Date",
|
||||
"page.sessions.table.ip": "IP Address",
|
||||
"page.sessions.table.user_agent": "User Agent",
|
||||
"page.sessions.table.actions": "Actions",
|
||||
"page.sessions.table.current_session": "Current Session",
|
||||
"page.api_keys.title": "API Keys",
|
||||
"page.api_keys.table.description": "Description",
|
||||
"page.api_keys.table.token": "Token",
|
||||
"page.api_keys.table.last_used_at": "Last Used",
|
||||
"page.api_keys.table.created_at": "Creation Date",
|
||||
"page.api_keys.table.actions": "Actions",
|
||||
"page.api_keys.never_used": "Never Used",
|
||||
"page.new_api_key.title": "New API Key",
|
||||
"page.offline.title": "Offline Mode",
|
||||
"page.offline.message": "You are offline",
|
||||
"page.offline.refresh_page": "Try to refresh the page",
|
||||
"page.webauthn_rename.title": "Rename Passkey",
|
||||
"page.cache.warning": "You're viewing a cached version. Refresh to see the latest one.",
|
||||
"alert.no_shared_entry": "There is no shared entry.",
|
||||
"alert.no_bookmark": "There are no starred entries.",
|
||||
"alert.no_category": "There is no category.",
|
||||
"alert.no_category_entry": "There are no entries in this category.",
|
||||
"alert.no_tag_entry": "There are no entries matching this tag.",
|
||||
"alert.no_feed_entry": "There are no entries for this feed.",
|
||||
"alert.no_feed": "You don’t have any feeds.",
|
||||
"alert.no_feed_in_category": "There is no feed for this category.",
|
||||
"alert.no_history": "There is no history at the moment.",
|
||||
"alert.feed_error": "There is a problem with this feed",
|
||||
"alert.no_search_result": "There are no results for this search.",
|
||||
"alert.no_unread_entry": "There are no unread entries.",
|
||||
"alert.no_user": "You are the only user.",
|
||||
"alert.account_unlinked": "Your external account is now dissociated!",
|
||||
"alert.account_linked": "Your external account is now linked!",
|
||||
"alert.pocket_linked": "Your Pocket account is now linked!",
|
||||
"alert.prefs_saved": "Preferences saved!",
|
||||
"error.unlink_account_without_password": "You must define a password otherwise you won’t be able to login again.",
|
||||
"error.duplicate_linked_account": "There is already someone associated with this provider!",
|
||||
"error.duplicate_fever_username": "There is already someone else with the same Fever username!",
|
||||
"error.duplicate_googlereader_username": "There is already someone else with the same Google Reader username!",
|
||||
"error.pocket_request_token": "Unable to fetch request token from Pocket!",
|
||||
"error.pocket_access_token": "Unable to fetch access token from Pocket!",
|
||||
"error.category_already_exists": "This category already exists.",
|
||||
"error.unable_to_create_category": "Unable to create this category.",
|
||||
"error.unable_to_update_category": "Unable to update this category.",
|
||||
"error.user_already_exists": "This user already exists.",
|
||||
"error.unable_to_create_user": "Unable to create this user.",
|
||||
"error.unable_to_update_user": "Unable to update this user.",
|
||||
"error.unable_to_update_feed": "Unable to update this feed.",
|
||||
"error.subscription_not_found": "Unable to find any feed.",
|
||||
"error.invalid_theme": "Invalid theme.",
|
||||
"error.invalid_language": "Invalid language.",
|
||||
"error.invalid_timezone": "Invalid timezone.",
|
||||
"error.invalid_entry_direction": "Invalid entry direction.",
|
||||
"error.invalid_display_mode": "Invalid web app display mode.",
|
||||
"error.invalid_gesture_nav": "Invalid gesture navigation.",
|
||||
"error.invalid_default_home_page": "Invalid default homepage!",
|
||||
"error.empty_file": "This file is empty.",
|
||||
"error.bad_credentials": "Invalid username or password.",
|
||||
"error.fields_mandatory": "All fields are mandatory.",
|
||||
"error.title_required": "The title is mandatory.",
|
||||
"error.different_passwords": "Passwords are not the same.",
|
||||
"error.password_min_length": "The password must have at least 6 characters.",
|
||||
"error.settings_mandatory_fields": "The username, theme, language and timezone fields are mandatory.",
|
||||
"error.settings_reading_speed_is_positive": "The reading speeds must be positive integers.",
|
||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
||||
"error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
|
||||
"error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
|
||||
"error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
|
||||
"error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
|
||||
"error.entries_per_page_invalid": "The number of entries per page is not valid.",
|
||||
"error.feed_mandatory_fields": "The URL and the category are mandatory.",
|
||||
"error.feed_already_exists": "This feed already exists.",
|
||||
"error.invalid_feed_url": "Invalid feed URL.",
|
||||
"error.invalid_site_url": "Invalid site URL.",
|
||||
"error.feed_url_not_empty": "The feed URL cannot be empty.",
|
||||
"error.site_url_not_empty": "The site URL cannot be empty.",
|
||||
"error.feed_title_not_empty": "The feed title cannot be empty.",
|
||||
"error.feed_category_not_found": "This category does not exist or does not belong to this user.",
|
||||
"error.feed_invalid_blocklist_rule": "The block list rule is invalid.",
|
||||
"error.feed_invalid_keeplist_rule": "The keep list rule is invalid.",
|
||||
"error.user_mandatory_fields": "The username is mandatory.",
|
||||
"error.api_key_already_exists": "This API Key already exists.",
|
||||
"error.unable_to_create_api_key": "Unable to create this API Key.",
|
||||
"form.feed.label.title": "Title",
|
||||
"form.feed.label.site_url": "Site URL",
|
||||
"form.feed.label.feed_url": "Feed URL",
|
||||
"form.feed.label.description": "Description",
|
||||
"form.feed.label.category": "Category",
|
||||
"form.feed.label.crawler": "Fetch original content",
|
||||
"form.feed.label.feed_username": "Feed Username",
|
||||
"form.feed.label.feed_password": "Feed Password",
|
||||
"form.feed.label.user_agent": "Override Default User Agent",
|
||||
"form.feed.label.cookie": "Set Cookies",
|
||||
"form.feed.label.scraper_rules": "Scraper Rules",
|
||||
"form.feed.label.rewrite_rules": "Rewrite Rules",
|
||||
"form.feed.label.apprise_service_urls": "Comma separated list of Apprise service URLs",
|
||||
"form.feed.label.blocklist_rules": "Block Rules",
|
||||
"form.feed.label.keeplist_rules": "Keep Rules",
|
||||
"form.feed.label.urlrewrite_rules": "URL Rewrite Rules",
|
||||
"form.feed.label.ignore_http_cache": "Ignore HTTP cache",
|
||||
"form.feed.label.allow_self_signed_certificates": "Allow self-signed or invalid certificates",
|
||||
"form.feed.label.disable_http2": "Disable HTTP/2 to avoid fingerprinting",
|
||||
"form.feed.label.fetch_via_proxy": "Fetch via proxy",
|
||||
"form.feed.label.disabled": "Do not refresh this feed",
|
||||
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||
"form.feed.label.hide_globally": "Hide entries in global unread list",
|
||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
||||
"form.feed.fieldset.general": "General",
|
||||
"form.feed.fieldset.rules": "Rules",
|
||||
"form.feed.fieldset.network_settings": "Network Settings",
|
||||
"form.feed.fieldset.integration": "Third-Party Services",
|
||||
"form.category.label.title": "Title",
|
||||
"form.category.hide_globally": "Hide entries in global unread list",
|
||||
"form.user.label.username": "Username",
|
||||
"form.user.label.password": "Password",
|
||||
"form.user.label.confirmation": "Password Confirmation",
|
||||
"form.user.label.admin": "Administrator",
|
||||
"form.prefs.label.language": "Language",
|
||||
"form.prefs.label.timezone": "Timezone",
|
||||
"form.prefs.label.theme": "Theme",
|
||||
"form.prefs.label.entry_sorting": "Entry sorting",
|
||||
"form.prefs.label.entries_per_page": "Entries per page",
|
||||
"form.prefs.label.default_reading_speed": "Reading speed for other languages (words per minute)",
|
||||
"form.prefs.label.cjk_reading_speed": "Reading speed for Chinese, Korean and Japanese (characters per minute)",
|
||||
"form.prefs.label.display_mode": "Progressive Web App (PWA) display mode",
|
||||
"form.prefs.select.older_first": "Older entries first",
|
||||
"form.prefs.select.recent_first": "Recent entries first",
|
||||
"form.prefs.select.fullscreen": "Fullscreen",
|
||||
"form.prefs.select.standalone": "Standalone",
|
||||
"form.prefs.select.minimal_ui": "Minimal",
|
||||
"form.prefs.select.browser": "Browser",
|
||||
"form.prefs.select.publish_time": "Entry published time",
|
||||
"form.prefs.select.created_time": "Entry created time",
|
||||
"form.prefs.select.alphabetical": "Alphabetical",
|
||||
"form.prefs.select.unread_count": "Unread count",
|
||||
"form.prefs.select.none": "None",
|
||||
"form.prefs.select.tap": "Double tap",
|
||||
"form.prefs.select.swipe": "Swipe",
|
||||
"form.prefs.label.keyboard_shortcuts": "Enable keyboard shortcuts",
|
||||
"form.prefs.label.entry_swipe": "Enable entry swipe on touch screens",
|
||||
"form.prefs.label.gesture_nav": "Gesture to navigate between entries",
|
||||
"form.prefs.label.show_reading_time": "Show estimated reading time for entries",
|
||||
"form.prefs.label.custom_css": "Custom CSS",
|
||||
"form.prefs.label.custom_js": "Custom JavaScript",
|
||||
"form.prefs.label.entry_order": "Entry sorting column",
|
||||
"form.prefs.label.default_home_page": "Default home page",
|
||||
"form.prefs.label.categories_sorting_order": "Categories sorting",
|
||||
"form.prefs.label.mark_read_on_view": "Automatically mark entries as read when viewed",
|
||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
|
||||
"form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
||||
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
||||
"form.prefs.label.cache_for_offline": "Cache entries for offline reading",
|
||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
||||
"form.prefs.label.external_font_hosts": "External font hosts",
|
||||
"form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
|
||||
"error.settings_invalid_domain_list": "Invalid domain list. Please provide a space separated list of domains.",
|
||||
"form.import.label.file": "OPML file",
|
||||
"form.import.label.url": "URL",
|
||||
"form.integration.betula_activate": "Save entries to Betula",
|
||||
"form.integration.betula_url": "Betula server URL",
|
||||
"form.integration.betula_token": "Betula Token",
|
||||
"form.integration.fever_activate": "Activate Fever API",
|
||||
"form.integration.fever_username": "Fever Username",
|
||||
"form.integration.fever_password": "Fever Password",
|
||||
"form.integration.fever_endpoint": "Fever API endpoint:",
|
||||
"form.integration.googlereader_activate": "Activate Google Reader API",
|
||||
"form.integration.googlereader_username": "Google Reader Username",
|
||||
"form.integration.googlereader_password": "Google Reader Password",
|
||||
"form.integration.googlereader_endpoint": "Google Reader API endpoint:",
|
||||
"form.integration.pinboard_activate": "Save entries to Pinboard",
|
||||
"form.integration.pinboard_token": "Pinboard API Token",
|
||||
"form.integration.pinboard_tags": "Pinboard Tags",
|
||||
"form.integration.pinboard_bookmark": "Mark bookmark as unread",
|
||||
"form.integration.instapaper_activate": "Save entries to Instapaper",
|
||||
"form.integration.instapaper_username": "Instapaper Username",
|
||||
"form.integration.instapaper_password": "Instapaper Password",
|
||||
"form.integration.pocket_activate": "Save entries to Pocket",
|
||||
"form.integration.pocket_consumer_key": "Pocket Consumer Key",
|
||||
"form.integration.pocket_access_token": "Pocket Access Token",
|
||||
"form.integration.pocket_connect_link": "Connect your Pocket account",
|
||||
"form.integration.wallabag_activate": "Save entries to Wallabag",
|
||||
"form.integration.wallabag_only_url": "Send only URL (instead of full content)",
|
||||
"form.integration.wallabag_endpoint": "Wallabag API Endpoint",
|
||||
"form.integration.wallabag_client_id": "Wallabag Client ID",
|
||||
"form.integration.wallabag_client_secret": "Wallabag Client Secret",
|
||||
"form.integration.wallabag_username": "Wallabag Username",
|
||||
"form.integration.wallabag_password": "Wallabag Password",
|
||||
"form.integration.notion_activate": "Save entries to Notion",
|
||||
"form.integration.notion_page_id": "Notion Page ID",
|
||||
"form.integration.notion_token": "Notion Secret Token",
|
||||
"form.integration.apprise_activate": "Push entries to Apprise",
|
||||
"form.integration.apprise_url": "Apprise API URL",
|
||||
"form.integration.apprise_services_url": "Comma separated list of Apprise service URLs",
|
||||
"form.integration.nunux_keeper_activate": "Save entries to Nunux Keeper",
|
||||
"form.integration.nunux_keeper_endpoint": "Nunux Keeper API Endpoint",
|
||||
"form.integration.nunux_keeper_api_key": "Nunux Keeper API key",
|
||||
"form.integration.omnivore_activate": "Save entries to Omnivore",
|
||||
"form.integration.omnivore_api_key": "Omnivore API key",
|
||||
"form.integration.omnivore_url": "Omnivore API Endpoint",
|
||||
"form.integration.espial_activate": "Save entries to Espial",
|
||||
"form.integration.espial_endpoint": "Espial API Endpoint",
|
||||
"form.integration.espial_api_key": "Espial API key",
|
||||
"form.integration.espial_tags": "Espial Tags",
|
||||
"form.integration.readwise_activate": "Save entries to Readwise Reader",
|
||||
"form.integration.readwise_api_key": "Readwise Reader Access Token",
|
||||
"form.integration.readwise_api_key_link": "Get your Readwise Access Token",
|
||||
"form.integration.telegram_bot_activate": "Push new entries to Telegram chat",
|
||||
"form.integration.telegram_bot_token": "Bot token",
|
||||
"form.integration.telegram_chat_id": "Chat ID",
|
||||
"form.integration.telegram_topic_id": "Topic ID",
|
||||
"form.integration.telegram_bot_disable_web_page_preview": "Disable web page preview",
|
||||
"form.integration.telegram_bot_disable_notification": "Disable notification",
|
||||
"form.integration.telegram_bot_disable_buttons": "Disable buttons",
|
||||
"form.integration.linkace_activate": "Save entries to LinkAce",
|
||||
"form.integration.linkace_endpoint": "LinkAce API Endpoint",
|
||||
"form.integration.linkace_api_key": "LinkAce API key",
|
||||
"form.integration.linkace_tags": "LinkAce Tags",
|
||||
"form.integration.linkace_is_private": "Mark link as private",
|
||||
"form.integration.linkace_check_disabled": "Disable link check",
|
||||
"form.integration.linkding_activate": "Save entries to Linkding",
|
||||
"form.integration.linkding_endpoint": "Linkding API Endpoint",
|
||||
"form.integration.linkding_api_key": "Linkding API key",
|
||||
"form.integration.linkding_tags": "Linkding Tags",
|
||||
"form.integration.linkding_bookmark": "Mark bookmark as unread",
|
||||
"form.integration.linkwarden_activate": "Save entries to Linkwarden",
|
||||
"form.integration.linkwarden_endpoint": "Linkwarden API Endpoint",
|
||||
"form.integration.linkwarden_api_key": "Linkwarden API key",
|
||||
"form.integration.matrix_bot_activate": "Push new entries to Matrix",
|
||||
"form.integration.matrix_bot_user": "Username for Matrix",
|
||||
"form.integration.matrix_bot_password": "Password for Matrix user",
|
||||
"form.integration.matrix_bot_url": "Matrix server URL",
|
||||
"form.integration.matrix_bot_chat_id": "ID of Matrix Room",
|
||||
"form.integration.raindrop_activate": "Save entries to Raindrop",
|
||||
"form.integration.raindrop_token": "(Test) Token",
|
||||
"form.integration.raindrop_collection_id": "Collection ID",
|
||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
||||
"form.integration.readeck_activate": "Save entries to readeck",
|
||||
"form.integration.readeck_endpoint": "Readeck URL",
|
||||
"form.integration.readeck_api_key": "Readeck API key",
|
||||
"form.integration.readeck_labels": "Readeck Labels",
|
||||
"form.integration.readeck_only_url": "Send only URL (instead of full content)",
|
||||
"form.integration.shiori_activate": "Save articles to Shiori",
|
||||
"form.integration.shiori_endpoint": "Shiori API Endpoint",
|
||||
"form.integration.shiori_username": "Shiori Username",
|
||||
"form.integration.shiori_password": "Shiori Password",
|
||||
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||
"form.integration.shaarli_endpoint": "Shaarli URL",
|
||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||
"form.integration.webhook_activate": "Enable Webhook",
|
||||
"form.integration.webhook_url": "Webhook URL",
|
||||
"form.integration.webhook_secret": "Webhook Secret",
|
||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
||||
"form.integration.ntfy_topic": "Ntfy topic",
|
||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
||||
"form.integration.cubox_activate": "Save entries to Cubox",
|
||||
"form.integration.cubox_api_link": "Cubox API link",
|
||||
"form.api_key.label.description": "API Key Label",
|
||||
"form.submit.loading": "Loading…",
|
||||
"form.submit.saving": "Saving…",
|
||||
"time_elapsed.not_yet": "not yet",
|
||||
"time_elapsed.yesterday": "yesterday",
|
||||
"time_elapsed.now": "just now",
|
||||
"time_elapsed.minutes": ["%d minute ago", "%d minutes ago"],
|
||||
"time_elapsed.hours": ["%d hour ago", "%d hours ago"],
|
||||
"time_elapsed.days": ["%d day ago", "%d days ago"],
|
||||
"time_elapsed.weeks": ["%d week ago", "%d weeks ago"],
|
||||
"time_elapsed.months": ["%d month ago", "%d months ago"],
|
||||
"time_elapsed.years": ["%d year ago", "%d years ago"],
|
||||
"alert.too_many_feeds_refresh": [
|
||||
"You have triggered too many feed refreshes. Please wait %d minute before trying again.",
|
||||
"You have triggered too many feed refreshes. Please wait %d minutes before trying again."
|
||||
],
|
||||
"alert.background_feed_refresh": "All feeds are being refreshed in the background. You can continue to use Miniflux while this process is running.",
|
||||
"error.http_response_too_large": "The HTTP response is too large. You could increase the HTTP response size limit in the global settings (requires a server restart).",
|
||||
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||
"error.tls_error": "TLS error: %q. You could disable TLS verification in the feed settings if you would like.",
|
||||
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
||||
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
||||
"error.http_client_error": "HTTP client error: %v.",
|
||||
"error.http_not_authorized": "Access to this website is not authorized. It could be a bad username or password.",
|
||||
"error.http_too_many_requests": "Miniflux generated too many requests to this website. Please, try again later or change the application configuration.",
|
||||
"error.http_forbidden": "Access to this website is forbidden. Perhaps, this website has a bot protection mechanism?",
|
||||
"error.http_resource_not_found": "The requested resource is not found. Please, verify the URL.",
|
||||
"error.http_internal_server_error": "The website is not available at the moment due to a server error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_bad_gateway": "The website is not available at the moment due to a bad gateway error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_service_unavailable": "The website is not available at the moment due to an internal server error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_gateway_timeout": "The website is not available at the moment due to a gateway timeout error. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.http_unexpected_status_code": "The website is not available at the moment due to an unexpected HTTP status code: %d. The problem is not on Miniflux side. Please, try again later.",
|
||||
"error.database_error": "Database error: %v.",
|
||||
"error.category_not_found": "This category does not exist or does not belong to this user.",
|
||||
"error.duplicated_feed": "This feed already exists.",
|
||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
||||
"form.prefs.label.media_playback_rate": "Playback speed of the audio/video",
|
||||
"error.settings_media_playback_rate_range": "Playback speed is out of range",
|
||||
"enclosure_media_controls.seek": "Seek:",
|
||||
"enclosure_media_controls.seek.title": "Seek %s seconds",
|
||||
"enclosure_media_controls.speed": "Speed:",
|
||||
"enclosure_media_controls.speed.faster": "Faster",
|
||||
"enclosure_media_controls.speed.faster.title": "Faster by %sx",
|
||||
"enclosure_media_controls.speed.slower": "Slower",
|
||||
"enclosure_media_controls.speed.slower.title": "Slower by %sx",
|
||||
"enclosure_media_controls.speed.reset": "Reset",
|
||||
"enclosure_media_controls.speed.reset.title": "Reset speed to 1x"
|
||||
}
|
|
@ -41,6 +41,7 @@ type User struct {
|
|||
MediaPlaybackRate float64 `json:"media_playback_rate"`
|
||||
BlockFilterEntryRules string `json:"block_filter_entry_rules"`
|
||||
KeepFilterEntryRules string `json:"keep_filter_entry_rules"`
|
||||
CacheForOffline bool `json:"cache_for_offline"`
|
||||
}
|
||||
|
||||
// UserCreationRequest represents the request to create a user.
|
||||
|
@ -82,6 +83,7 @@ type UserModificationRequest struct {
|
|||
MediaPlaybackRate *float64 `json:"media_playback_rate"`
|
||||
BlockFilterEntryRules *string `json:"block_filter_entry_rules"`
|
||||
KeepFilterEntryRules *string `json:"keep_filter_entry_rules"`
|
||||
CacheForOffline *bool `json:"cache_for_offline"`
|
||||
}
|
||||
|
||||
// Patch updates the User object with the modification request.
|
||||
|
@ -197,6 +199,9 @@ func (u *UserModificationRequest) Patch(user *User) {
|
|||
if u.KeepFilterEntryRules != nil {
|
||||
user.KeepFilterEntryRules = *u.KeepFilterEntryRules
|
||||
}
|
||||
if u.CacheForOffline != nil {
|
||||
user.CacheForOffline = *u.CacheForOffline
|
||||
}
|
||||
}
|
||||
|
||||
// UseTimezone converts last login date to the given timezone.
|
||||
|
|
|
@ -241,7 +241,7 @@ func sanitizeAttributes(baseURL, tagName string, attributes []html.Attribute) ([
|
|||
func getExtraAttributes(tagName string) ([]string, []string) {
|
||||
switch tagName {
|
||||
case "a":
|
||||
return []string{"rel", "target", "referrerpolicy"}, []string{`rel="noopener noreferrer"`, `target="_blank"`, `referrerpolicy="no-referrer"`}
|
||||
return []string{"rel", "target", "referrerpolicy"}, []string{`rel="noopener noreferrer"`, `target="_blank"`, `referrerpolicy="strict-origin"`}
|
||||
case "video", "audio":
|
||||
return []string{"controls"}, []string{"controls"}
|
||||
case "iframe":
|
||||
|
|
|
@ -96,7 +96,8 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m
|
|||
mark_read_on_view,
|
||||
media_playback_rate,
|
||||
block_filter_entry_rules,
|
||||
keep_filter_entry_rules
|
||||
keep_filter_entry_rules,
|
||||
cache_for_offline
|
||||
`
|
||||
|
||||
tx, err := s.db.Begin()
|
||||
|
@ -140,6 +141,7 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m
|
|||
&user.MediaPlaybackRate,
|
||||
&user.BlockFilterEntryRules,
|
||||
&user.KeepFilterEntryRules,
|
||||
&user.CacheForOffline,
|
||||
)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
|
@ -204,9 +206,10 @@ func (s *Storage) UpdateUser(user *model.User) error {
|
|||
mark_read_on_media_player_completion=$25,
|
||||
media_playback_rate=$26,
|
||||
block_filter_entry_rules=$27,
|
||||
keep_filter_entry_rules=$28
|
||||
keep_filter_entry_rules=$28,
|
||||
cache_for_offline=$29
|
||||
WHERE
|
||||
id=$29
|
||||
id=$30
|
||||
`
|
||||
|
||||
_, err = s.db.Exec(
|
||||
|
@ -239,6 +242,7 @@ func (s *Storage) UpdateUser(user *model.User) error {
|
|||
user.MediaPlaybackRate,
|
||||
user.BlockFilterEntryRules,
|
||||
user.KeepFilterEntryRules,
|
||||
user.CacheForOffline,
|
||||
user.ID,
|
||||
)
|
||||
if err != nil {
|
||||
|
@ -273,9 +277,10 @@ func (s *Storage) UpdateUser(user *model.User) error {
|
|||
mark_read_on_media_player_completion=$24,
|
||||
media_playback_rate=$25,
|
||||
block_filter_entry_rules=$26,
|
||||
keep_filter_entry_rules=$27
|
||||
keep_filter_entry_rules=$27,
|
||||
cache_for_offline=$28
|
||||
WHERE
|
||||
id=$28
|
||||
id=$29
|
||||
`
|
||||
|
||||
_, err := s.db.Exec(
|
||||
|
@ -307,6 +312,7 @@ func (s *Storage) UpdateUser(user *model.User) error {
|
|||
user.MediaPlaybackRate,
|
||||
user.BlockFilterEntryRules,
|
||||
user.KeepFilterEntryRules,
|
||||
user.CacheForOffline,
|
||||
user.ID,
|
||||
)
|
||||
|
||||
|
@ -360,7 +366,8 @@ func (s *Storage) UserByID(userID int64) (*model.User, error) {
|
|||
mark_read_on_media_player_completion,
|
||||
media_playback_rate,
|
||||
block_filter_entry_rules,
|
||||
keep_filter_entry_rules
|
||||
keep_filter_entry_rules,
|
||||
cache_for_offline
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
|
@ -401,7 +408,8 @@ func (s *Storage) UserByUsername(username string) (*model.User, error) {
|
|||
mark_read_on_media_player_completion,
|
||||
media_playback_rate,
|
||||
block_filter_entry_rules,
|
||||
keep_filter_entry_rules
|
||||
keep_filter_entry_rules,
|
||||
cache_for_offline
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
|
@ -442,7 +450,8 @@ func (s *Storage) UserByField(field, value string) (*model.User, error) {
|
|||
mark_read_on_media_player_completion,
|
||||
media_playback_rate,
|
||||
block_filter_entry_rules,
|
||||
keep_filter_entry_rules
|
||||
keep_filter_entry_rules,
|
||||
cache_for_offline
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
|
@ -490,7 +499,8 @@ func (s *Storage) UserByAPIKey(token string) (*model.User, error) {
|
|||
u.mark_read_on_media_player_completion,
|
||||
media_playback_rate,
|
||||
u.block_filter_entry_rules,
|
||||
u.keep_filter_entry_rules
|
||||
u.keep_filter_entry_rules,
|
||||
u.cache_for_offline
|
||||
FROM
|
||||
users u
|
||||
LEFT JOIN
|
||||
|
@ -533,6 +543,7 @@ func (s *Storage) fetchUser(query string, args ...interface{}) (*model.User, err
|
|||
&user.MediaPlaybackRate,
|
||||
&user.BlockFilterEntryRules,
|
||||
&user.KeepFilterEntryRules,
|
||||
&user.CacheForOffline,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
|
@ -646,7 +657,9 @@ func (s *Storage) Users() (model.Users, error) {
|
|||
mark_read_on_media_player_completion,
|
||||
media_playback_rate,
|
||||
block_filter_entry_rules,
|
||||
keep_filter_entry_rules
|
||||
keep_filter_entry_rules,
|
||||
media_playback_rate,
|
||||
cache_for_offline
|
||||
FROM
|
||||
users
|
||||
ORDER BY username ASC
|
||||
|
@ -690,6 +703,7 @@ func (s *Storage) Users() (model.Users, error) {
|
|||
&user.MediaPlaybackRate,
|
||||
&user.BlockFilterEntryRules,
|
||||
&user.KeepFilterEntryRules,
|
||||
&user.CacheForOffline,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
|
|
|
@ -132,6 +132,7 @@
|
|||
{{ if .flashErrorMessage }}
|
||||
<div role="alert" class="flash-error-message alert alert-error">{{ .flashErrorMessage }}</div>
|
||||
{{ end }}
|
||||
<div role="alert" aria-live="assertive" aria-atomic="true" class="flash-message alert alert-warning offline-hidden">{{ t "page.cache.warning" }}</div>
|
||||
|
||||
{{template "page_header" .}}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<section class="entry" data-id="{{ .entry.ID }}" aria-labelledby="page-header-title">
|
||||
<header class="entry-header">
|
||||
<h1 id="page-header-title" dir="auto">
|
||||
<a href="{{ .entry.URL | safeURL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer">{{ .entry.Title }}</a>
|
||||
<a href="{{ .entry.URL | safeURL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="strict-origin">{{ .entry.Title }}</a>
|
||||
</h1>
|
||||
{{ if .user }}
|
||||
<div class="entry-actions">
|
||||
|
@ -79,7 +79,7 @@
|
|||
class="page-link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
referrerpolicy="no-referrer"
|
||||
referrerpolicy="strict-origin"
|
||||
data-original-link="{{ .user.MarkReadOnView }}">{{ icon "external-link" }}<span class="icon-label">{{ t "entry.external_link.label" }}</span></a>
|
||||
</li>
|
||||
<li>
|
||||
|
@ -98,7 +98,7 @@
|
|||
title="{{ t "entry.comments.title" }}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
referrerpolicy="no-referrer"
|
||||
referrerpolicy="strict-origin"
|
||||
data-comments-link="true"
|
||||
>{{ icon "comment" }}<span class="icon-label">{{ t "entry.comments.label" }}</span></a>
|
||||
</li>
|
||||
|
@ -232,7 +232,7 @@
|
|||
{{ end }}
|
||||
|
||||
<div class="entry-enclosure-download">
|
||||
<a href="{{ .URL | safeURL }}" title="{{ t "action.download" }}{{ if gt .Size 0 }} - {{ formatFileSize .Size }}{{ end }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer">{{ .URL | safeURL }}</a>
|
||||
<a href="{{ .URL | safeURL }}" title="{{ t "action.download" }}{{ if gt .Size 0 }} - {{ formatFileSize .Size }}{{ end }}" target="_blank" rel="noopener noreferrer" referrerpolicy="strict-origin">{{ .URL | safeURL }}</a>
|
||||
<small>{{ if gt .Size 0 }} - <strong>{{ formatFileSize .Size }}</strong>{{ end }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -66,7 +66,7 @@ func (h *handler) showUnreadEntryPage(w http.ResponseWriter, r *http.Request) {
|
|||
prevEntryRoute = route.Path(h.router, "unreadEntry", "entryID", prevEntry.ID)
|
||||
}
|
||||
|
||||
if entry.ShouldMarkAsReadOnView(user) {
|
||||
if entry.ShouldMarkAsReadOnView(user) && !request.IsServiceWorker(r) {
|
||||
entry.Status = model.EntryStatusRead
|
||||
}
|
||||
|
||||
|
@ -90,6 +90,7 @@ func (h *handler) showUnreadEntryPage(w http.ResponseWriter, r *http.Request) {
|
|||
view.Set("user", user)
|
||||
view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID))
|
||||
view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
|
||||
view.Set("useCachedVersion", r.Header.Get("X-Cache-Hit") != "")
|
||||
|
||||
// Fetching the counter here avoid to be off by one.
|
||||
view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
|
||||
|
|
|
@ -52,6 +52,7 @@ type SettingsForm struct {
|
|||
MediaPlaybackRate float64
|
||||
BlockFilterEntryRules string
|
||||
KeepFilterEntryRules string
|
||||
CacheForOffline bool
|
||||
}
|
||||
|
||||
// MarkAsReadBehavior returns the MarkReadBehavior from the given MarkReadOnView and MarkReadOnMediaPlayerCompletion values.
|
||||
|
@ -119,6 +120,8 @@ func (s *SettingsForm) Merge(user *model.User) *model.User {
|
|||
user.MarkReadOnView = MarkReadOnView
|
||||
user.MarkReadOnMediaPlayerCompletion = MarkReadOnMediaPlayerCompletion
|
||||
|
||||
user.CacheForOffline = s.CacheForOffline
|
||||
|
||||
if s.Password != "" {
|
||||
user.Password = s.Password
|
||||
}
|
||||
|
@ -205,5 +208,6 @@ func NewSettingsForm(r *http.Request) *SettingsForm {
|
|||
MediaPlaybackRate: mediaPlaybackRate,
|
||||
BlockFilterEntryRules: r.FormValue("block_filter_entry_rules"),
|
||||
KeepFilterEntryRules: r.FormValue("keep_filter_entry_rules"),
|
||||
CacheForOffline: r.FormValue("cache_for_offline") == "1",
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ func (h *handler) showSettingsPage(w http.ResponseWriter, r *http.Request) {
|
|||
MediaPlaybackRate: user.MediaPlaybackRate,
|
||||
BlockFilterEntryRules: user.BlockFilterEntryRules,
|
||||
KeepFilterEntryRules: user.KeepFilterEntryRules,
|
||||
CacheForOffline: user.CacheForOffline,
|
||||
}
|
||||
|
||||
timezones, err := h.store.Timezones()
|
||||
|
|
|
@ -24,7 +24,9 @@ hr {
|
|||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
color: var(--title-color);
|
||||
}
|
||||
|
||||
|
@ -75,7 +77,8 @@ a:hover {
|
|||
padding: var(--padding-size);
|
||||
position: absolute;
|
||||
transition: translate 0.3s;
|
||||
translate: -50% calc(-100% - calc(var(--padding-size) * 2) - calc(var(--border-size) * 2));
|
||||
translate: -50%
|
||||
calc(-100% - calc(var(--padding-size) * 2) - calc(var(--border-size) * 2));
|
||||
}
|
||||
|
||||
.skip-to-content-link:focus {
|
||||
|
@ -274,11 +277,30 @@ a:hover {
|
|||
}
|
||||
|
||||
@keyframes toastKeyFrames {
|
||||
0% {visibility: hidden; opacity: 0;}
|
||||
25% {visibility: visible; opacity: 1; z-index: 9999}
|
||||
50% {visibility: visible; opacity: 1; z-index: 9999}
|
||||
75% {visibility: visible; opacity: 1; z-index: 9999}
|
||||
100% {visibility: hidden; opacity: 0; z-index: 0}
|
||||
0% {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
25% {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
z-index: 9999;
|
||||
}
|
||||
50% {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
z-index: 9999;
|
||||
}
|
||||
75% {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
z-index: 9999;
|
||||
}
|
||||
100% {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Hide the logo when there is not enough space to display menus when using languages more verbose than English */
|
||||
|
@ -309,7 +331,7 @@ a:hover {
|
|||
padding: 0 12px 0 0;
|
||||
line-height: normal;
|
||||
border: none;
|
||||
font-size: 1.0em;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.header nav {
|
||||
|
@ -321,7 +343,8 @@ a:hover {
|
|||
display: none;
|
||||
}
|
||||
|
||||
.header ul:not(.js-menu-show), .header ul.js-menu-show {
|
||||
.header ul:not(.js-menu-show),
|
||||
.header ul.js-menu-show {
|
||||
display: revert;
|
||||
}
|
||||
|
||||
|
@ -347,11 +370,14 @@ table {
|
|||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table, th, td {
|
||||
table,
|
||||
th,
|
||||
td {
|
||||
border: 1px solid var(--table-border-color);
|
||||
}
|
||||
|
||||
th, td {
|
||||
th,
|
||||
td {
|
||||
padding: 5px;
|
||||
text-align: left;
|
||||
}
|
||||
|
@ -642,7 +668,7 @@ template {
|
|||
right: 0;
|
||||
font-size: 1.7em;
|
||||
color: #ccc;
|
||||
padding:0 .2em;
|
||||
padding: 0 0.2em;
|
||||
margin: 10px;
|
||||
text-decoration: none;
|
||||
background-color: transparent;
|
||||
|
@ -706,7 +732,6 @@ template {
|
|||
color: var(--category-link-hover-color);
|
||||
}
|
||||
|
||||
|
||||
.category-item-total {
|
||||
color: var(--body-color);
|
||||
}
|
||||
|
@ -746,7 +771,8 @@ template {
|
|||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.pagination-next, .pagination-last {
|
||||
.pagination-next,
|
||||
.pagination-last {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
|
@ -784,7 +810,8 @@ template {
|
|||
}
|
||||
|
||||
.item.current-item {
|
||||
border: var(--current-item-border-width) solid var(--current-item-border-color);
|
||||
border: var(--current-item-border-width) solid
|
||||
var(--current-item-border-color);
|
||||
padding: 3px;
|
||||
box-shadow: var(--current-item-box-shadow);
|
||||
}
|
||||
|
@ -793,7 +820,6 @@ template {
|
|||
outline: none;
|
||||
}
|
||||
|
||||
|
||||
.item-header {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
@ -948,7 +974,7 @@ article.category-has-unread {
|
|||
}
|
||||
|
||||
.entry header h1 {
|
||||
font-size: 2.0em;
|
||||
font-size: 2em;
|
||||
line-height: 1.25em;
|
||||
margin: 5px 0 30px 0;
|
||||
overflow-wrap: break-word;
|
||||
|
@ -1026,7 +1052,12 @@ article.category-has-unread {
|
|||
touch-action: pan-y pinch-zoom;
|
||||
}
|
||||
|
||||
.entry-content h1, h2, h3, h4, h5, h6 {
|
||||
.entry-content h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin-top: 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
@ -1242,12 +1273,13 @@ details.entry-enclosures {
|
|||
opacity: 20%;
|
||||
}
|
||||
|
||||
audio, video {
|
||||
audio,
|
||||
video {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.media-controls {
|
||||
font-size: .9em;
|
||||
font-size: 0.9em;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
@ -1291,6 +1323,7 @@ audio, video {
|
|||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
.hidden,
|
||||
.offline-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -30,12 +30,18 @@ function checkMenuToggleModeByLayout() {
|
|||
|
||||
if (document.documentElement.clientWidth < 620) {
|
||||
const navMenuElement = document.getElementById("header-menu");
|
||||
const navMenuElementIsExpanded = navMenuElement.classList.contains("js-menu-show");
|
||||
const logoToggleButtonLabel = logoElement.getAttribute("data-toggle-button-label");
|
||||
const navMenuElementIsExpanded =
|
||||
navMenuElement.classList.contains("js-menu-show");
|
||||
const logoToggleButtonLabel = logoElement.getAttribute(
|
||||
"data-toggle-button-label",
|
||||
);
|
||||
logoElement.setAttribute("role", "button");
|
||||
logoElement.setAttribute("tabindex", "0");
|
||||
logoElement.setAttribute("aria-label", logoToggleButtonLabel);
|
||||
logoElement.setAttribute("aria-expanded", navMenuElementIsExpanded?"true":"false");
|
||||
logoElement.setAttribute(
|
||||
"aria-expanded",
|
||||
navMenuElementIsExpanded ? "true" : "false",
|
||||
);
|
||||
homePageLinkElement.setAttribute("tabindex", "-1");
|
||||
} else {
|
||||
logoElement.removeAttribute("role");
|
||||
|
@ -50,17 +56,26 @@ function fixVoiceOverDetailsSummaryBug() {
|
|||
document.querySelectorAll("details").forEach((details) => {
|
||||
const summaryElement = details.querySelector("summary");
|
||||
summaryElement.setAttribute("role", "button");
|
||||
summaryElement.setAttribute("aria-expanded", details.open? "true": "false");
|
||||
summaryElement.setAttribute(
|
||||
"aria-expanded",
|
||||
details.open ? "true" : "false",
|
||||
);
|
||||
|
||||
details.addEventListener("toggle", () => {
|
||||
summaryElement.setAttribute("aria-expanded", details.open? "true": "false");
|
||||
summaryElement.setAttribute(
|
||||
"aria-expanded",
|
||||
details.open ? "true" : "false",
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Show and hide the main menu on mobile devices.
|
||||
function toggleMainMenu(event) {
|
||||
if (event.type === "keydown" && !(event.key === "Enter" || event.key === " ")) {
|
||||
if (
|
||||
event.type === "keydown" &&
|
||||
!(event.key === "Enter" || event.key === " ")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -125,7 +140,9 @@ function markPageAsRead() {
|
|||
updateEntriesStatus(entryIDs, "read", () => {
|
||||
// Make sure the Ajax request reach the server before we reload the page.
|
||||
|
||||
const element = document.querySelector(":is(a, button)[data-action=markPageAsRead]");
|
||||
const element = document.querySelector(
|
||||
":is(a, button)[data-action=markPageAsRead]",
|
||||
);
|
||||
let showOnlyUnread = false;
|
||||
if (element) {
|
||||
showOnlyUnread = element.dataset.showOnlyUnread || false;
|
||||
|
@ -148,6 +165,7 @@ function markPageAsRead() {
|
|||
* @param {boolean} setToRead
|
||||
*/
|
||||
function handleEntryStatus(item, element, setToRead) {
|
||||
<<<<<<< HEAD
|
||||
const toasting = !element;
|
||||
const currentEntry = findEntry(element);
|
||||
if (currentEntry) {
|
||||
|
@ -164,13 +182,35 @@ function handleEntryStatus(item, element, setToRead) {
|
|||
break;
|
||||
}
|
||||
}
|
||||
=======
|
||||
const toasting = !element;
|
||||
const currentEntry = findEntry(element);
|
||||
if (currentEntry) {
|
||||
if (
|
||||
!setToRead ||
|
||||
currentEntry.querySelector(":is(a, button)[data-toggle-status]").dataset
|
||||
.value == "unread"
|
||||
) {
|
||||
toggleEntryStatus(currentEntry, toasting);
|
||||
>>>>>>> 55d8ddfa (PWA: First implementation of offline mode)
|
||||
}
|
||||
if (isListView() && currentEntry.classList.contains("current-item")) {
|
||||
switch (item) {
|
||||
case "previous":
|
||||
goToListItem(-1);
|
||||
break;
|
||||
case "next":
|
||||
goToListItem(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add an icon-label span element.
|
||||
function appendIconLabel(element, labelTextContent) {
|
||||
const span = document.createElement('span');
|
||||
span.classList.add('icon-label');
|
||||
const span = document.createElement("span");
|
||||
span.classList.add("icon-label");
|
||||
span.textContent = labelTextContent;
|
||||
element.appendChild(span);
|
||||
}
|
||||
|
@ -237,7 +277,7 @@ function updateEntriesStatus(entryIDs, status, callback) {
|
|||
const request = new RequestBuilder(url);
|
||||
request.withBody({ entry_ids: entryIDs, status: status });
|
||||
request.withCallback((resp) => {
|
||||
resp.json().then(count => {
|
||||
resp.json().then((count) => {
|
||||
if (callback) {
|
||||
callback(resp);
|
||||
}
|
||||
|
@ -257,7 +297,10 @@ function handleSaveEntry(element) {
|
|||
const toasting = !element;
|
||||
const currentEntry = findEntry(element);
|
||||
if (currentEntry) {
|
||||
saveEntry(currentEntry.querySelector(":is(a, button)[data-save-entry]"), toasting);
|
||||
saveEntry(
|
||||
currentEntry.querySelector(":is(a, button)[data-save-entry]"),
|
||||
toasting,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -270,6 +313,12 @@ function saveEntry(element, toasting) {
|
|||
element.textContent = "";
|
||||
appendIconLabel(element, element.dataset.labelLoading);
|
||||
|
||||
const request = new RequestBuilder(element.dataset.saveUrl);
|
||||
request.withCallback(() => {
|
||||
element.textContent = "";
|
||||
<<<<<<< HEAD
|
||||
appendIconLabel(element, element.dataset.labelLoading);
|
||||
|
||||
const request = new RequestBuilder(element.dataset.saveUrl);
|
||||
request.withCallback(() => {
|
||||
element.textContent = "";
|
||||
|
@ -281,6 +330,16 @@ function saveEntry(element, toasting) {
|
|||
}
|
||||
});
|
||||
request.execute();
|
||||
=======
|
||||
appendIconLabel(element, element.dataset.labelDone);
|
||||
element.dataset.completed = true;
|
||||
if (toasting) {
|
||||
const iconElement = document.querySelector("template#icon-save");
|
||||
showToast(element.dataset.toastDone, iconElement);
|
||||
}
|
||||
});
|
||||
request.execute();
|
||||
>>>>>>> 55d8ddfa (PWA: First implementation of offline mode)
|
||||
}
|
||||
|
||||
// Handle bookmark from the list view and entry view.
|
||||
|
@ -294,7 +353,9 @@ function handleBookmark(element) {
|
|||
|
||||
// Send the Ajax request and change the icon when bookmarking an entry.
|
||||
function toggleBookmark(parentElement, toasting) {
|
||||
const buttonElement = parentElement.querySelector(":is(a, button)[data-toggle-bookmark]");
|
||||
const buttonElement = parentElement.querySelector(
|
||||
":is(a, button)[data-toggle-bookmark]",
|
||||
);
|
||||
if (!buttonElement) {
|
||||
return;
|
||||
}
|
||||
|
@ -335,7 +396,9 @@ function handleFetchOriginalContent() {
|
|||
return;
|
||||
}
|
||||
|
||||
const buttonElement = document.querySelector(":is(a, button)[data-fetch-content-entry]");
|
||||
const buttonElement = document.querySelector(
|
||||
":is(a, button)[data-fetch-content-entry]",
|
||||
);
|
||||
if (!buttonElement) {
|
||||
return;
|
||||
}
|
||||
|
@ -347,13 +410,19 @@ function handleFetchOriginalContent() {
|
|||
|
||||
const request = new RequestBuilder(buttonElement.dataset.fetchContentUrl);
|
||||
request.withCallback((response) => {
|
||||
buttonElement.textContent = '';
|
||||
buttonElement.textContent = "";
|
||||
buttonElement.appendChild(previousElement);
|
||||
|
||||
response.json().then((data) => {
|
||||
if (data.hasOwnProperty("content") && data.hasOwnProperty("reading_time")) {
|
||||
document.querySelector(".entry-content").innerHTML = ttpolicy.createHTML(data.content);
|
||||
const entryReadingtimeElement = document.querySelector(".entry-reading-time");
|
||||
if (
|
||||
data.hasOwnProperty("content") &&
|
||||
data.hasOwnProperty("reading_time")
|
||||
) {
|
||||
document.querySelector(".entry-content").innerHTML =
|
||||
ttpolicy.createHTML(data.content);
|
||||
const entryReadingtimeElement = document.querySelector(
|
||||
".entry-reading-time",
|
||||
);
|
||||
if (entryReadingtimeElement) {
|
||||
entryReadingtimeElement.textContent = data.reading_time;
|
||||
}
|
||||
|
@ -374,20 +443,35 @@ function openOriginalLink(openLinkInCurrentTab) {
|
|||
return;
|
||||
}
|
||||
|
||||
const currentItemOriginalLink = document.querySelector(".current-item :is(a, button)[data-original-link]");
|
||||
const currentItemOriginalLink = document.querySelector(
|
||||
".current-item :is(a, button)[data-original-link]",
|
||||
);
|
||||
if (currentItemOriginalLink !== null) {
|
||||
DomHelper.openNewTab(currentItemOriginalLink.getAttribute("href"));
|
||||
|
||||
<<<<<<< HEAD
|
||||
const currentItem = document.querySelector(".current-item");
|
||||
// If we are not on the list of starred items, move to the next item
|
||||
if (document.location.href !== document.querySelector(':is(a, button)[data-page=starred]').href) {
|
||||
goToListItem(1);
|
||||
}
|
||||
markEntryAsRead(currentItem);
|
||||
=======
|
||||
const currentItem = document.querySelector(".current-item");
|
||||
// If we are not on the list of starred items, move to the next item
|
||||
if (
|
||||
document.location.href !=
|
||||
document.querySelector(":is(a, button)[data-page=starred]").href
|
||||
) {
|
||||
goToListItem(1);
|
||||
>>>>>>> 55d8ddfa (PWA: First implementation of offline mode)
|
||||
}
|
||||
markEntryAsRead(currentItem);
|
||||
}
|
||||
}
|
||||
|
||||
function openCommentLink(openLinkInCurrentTab) {
|
||||
<<<<<<< HEAD
|
||||
if (!isListView()) {
|
||||
const entryLink = document.querySelector(":is(a, button)[data-comments-link]");
|
||||
if (entryLink !== null) {
|
||||
|
@ -402,6 +486,27 @@ function openCommentLink(openLinkInCurrentTab) {
|
|||
if (currentItemCommentsLink !== null) {
|
||||
DomHelper.openNewTab(currentItemCommentsLink.getAttribute("href"));
|
||||
}
|
||||
=======
|
||||
if (!isListView()) {
|
||||
const entryLink = document.querySelector(
|
||||
":is(a, button)[data-comments-link]",
|
||||
);
|
||||
if (entryLink !== null) {
|
||||
if (openLinkInCurrentTab) {
|
||||
window.location.href = entryLink.getAttribute("href");
|
||||
} else {
|
||||
DomHelper.openNewTab(entryLink.getAttribute("href"));
|
||||
}
|
||||
return;
|
||||
>>>>>>> 55d8ddfa (PWA: First implementation of offline mode)
|
||||
}
|
||||
} else {
|
||||
const currentItemCommentsLink = document.querySelector(
|
||||
".current-item :is(a, button)[data-comments-link]",
|
||||
);
|
||||
if (currentItemCommentsLink !== null) {
|
||||
DomHelper.openNewTab(currentItemCommentsLink.getAttribute("href"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -413,7 +518,9 @@ function openSelectedItem() {
|
|||
}
|
||||
|
||||
function unsubscribeFromFeed() {
|
||||
const unsubscribeLinks = document.querySelectorAll("[data-action=remove-feed]");
|
||||
const unsubscribeLinks = document.querySelectorAll(
|
||||
"[data-action=remove-feed]",
|
||||
);
|
||||
if (unsubscribeLinks.length === 1) {
|
||||
const unsubscribeLink = unsubscribeLinks[0];
|
||||
|
||||
|
@ -433,8 +540,15 @@ function unsubscribeFromFeed() {
|
|||
* @param {string} page Page to redirect to.
|
||||
* @param {boolean} fallbackSelf Refresh actual page if the page is not found.
|
||||
*/
|
||||
<<<<<<< HEAD
|
||||
function goToPage(page, fallbackSelf = false) {
|
||||
const element = document.querySelector(":is(a, button)[data-page=" + page + "]");
|
||||
=======
|
||||
function goToPage(page, fallbackSelf) {
|
||||
const element = document.querySelector(
|
||||
":is(a, button)[data-page=" + page + "]",
|
||||
);
|
||||
>>>>>>> 55d8ddfa (PWA: First implementation of offline mode)
|
||||
|
||||
if (element) {
|
||||
document.location.href = element.href;
|
||||
|
@ -477,7 +591,7 @@ function goToFeedOrFeeds() {
|
|||
if (isEntry()) {
|
||||
goToFeed();
|
||||
} else {
|
||||
goToPage('feeds');
|
||||
goToPage("feeds");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -488,7 +602,9 @@ function goToFeed() {
|
|||
window.location.href = feedAnchor.href;
|
||||
}
|
||||
} else {
|
||||
const currentItemFeed = document.querySelector(".current-item :is(a, button)[data-feed-link]");
|
||||
const currentItemFeed = document.querySelector(
|
||||
".current-item :is(a, button)[data-feed-link]",
|
||||
);
|
||||
if (currentItemFeed !== null) {
|
||||
window.location.href = currentItemFeed.getAttribute("href");
|
||||
}
|
||||
|
@ -503,6 +619,7 @@ const BOTTOM = -9999;
|
|||
* @param {number} offset How many items to jump for focus.
|
||||
*/
|
||||
function goToListItem(offset) {
|
||||
<<<<<<< HEAD
|
||||
const items = DomHelper.getVisibleElements(".items .item");
|
||||
if (items.length === 0) {
|
||||
return;
|
||||
|
@ -534,6 +651,39 @@ function goToListItem(offset) {
|
|||
|
||||
break;
|
||||
}
|
||||
=======
|
||||
const items = DomHelper.getVisibleElements(".items .item");
|
||||
if (items.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (document.querySelector(".current-item") === null) {
|
||||
items[0].classList.add("current-item");
|
||||
items[0].focus();
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].classList.contains("current-item")) {
|
||||
items[i].classList.remove("current-item");
|
||||
|
||||
// By default adjust selection by offset
|
||||
let itemOffset = (i + offset + items.length) % items.length;
|
||||
// Allow jumping to top or bottom
|
||||
if (offset == TOP) {
|
||||
itemOffset = 0;
|
||||
} else if (offset == BOTTOM) {
|
||||
itemOffset = items.length - 1;
|
||||
}
|
||||
const item = items[itemOffset];
|
||||
|
||||
item.classList.add("current-item");
|
||||
DomHelper.scrollPageTo(item);
|
||||
item.focus();
|
||||
|
||||
break;
|
||||
>>>>>>> 55d8ddfa (PWA: First implementation of offline mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -562,15 +712,15 @@ function updateUnreadCounterValue(callback) {
|
|||
element.textContent = callback(oldValue);
|
||||
});
|
||||
|
||||
if (window.location.href.endsWith('/unread')) {
|
||||
const oldValue = parseInt(document.title.split('(')[1], 10);
|
||||
if (window.location.href.endsWith("/unread")) {
|
||||
const oldValue = parseInt(document.title.split("(")[1], 10);
|
||||
const newValue = callback(oldValue);
|
||||
|
||||
document.title = document.title.replace(
|
||||
/(.*?)\(\d+\)(.*?)/,
|
||||
function (match, prefix, suffix, offset, string) {
|
||||
return prefix + '(' + newValue + ')' + suffix;
|
||||
}
|
||||
return prefix + "(" + newValue + ")" + suffix;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -594,8 +744,12 @@ function findEntry(element) {
|
|||
}
|
||||
|
||||
function handleConfirmationMessage(linkElement, callback) {
|
||||
<<<<<<< HEAD
|
||||
if (linkElement.tagName !== 'A' && linkElement.tagName !== "BUTTON") {
|
||||
linkElement = linkElement.parentNode;
|
||||
=======
|
||||
if (linkElement.tagName != "A" && linkElement.tagName != "BUTTON") {
|
||||
linkElement = linkElement.parentNode;
|
||||
}
|
||||
|
||||
linkElement.style.display = "none";
|
||||
|
@ -606,7 +760,9 @@ function handleConfirmationMessage(linkElement, callback) {
|
|||
function createLoadingElement() {
|
||||
const loadingElement = document.createElement("span");
|
||||
loadingElement.className = "loading";
|
||||
loadingElement.appendChild(document.createTextNode(linkElement.dataset.labelLoading));
|
||||
loadingElement.appendChild(
|
||||
document.createTextNode(linkElement.dataset.labelLoading),
|
||||
);
|
||||
|
||||
questionElement.remove();
|
||||
containerElement.appendChild(loadingElement);
|
||||
|
@ -635,11 +791,14 @@ function handleConfirmationMessage(linkElement, callback) {
|
|||
} else {
|
||||
linkElement.style.display = "inline";
|
||||
questionElement.remove();
|
||||
>>>>>>> 55d8ddfa (PWA: First implementation of offline mode)
|
||||
}
|
||||
};
|
||||
|
||||
questionElement.className = "confirm";
|
||||
questionElement.appendChild(document.createTextNode(linkElement.dataset.labelQuestion + " "));
|
||||
questionElement.appendChild(
|
||||
document.createTextNode(linkElement.dataset.labelQuestion + " "),
|
||||
);
|
||||
questionElement.appendChild(yesElement);
|
||||
questionElement.appendChild(document.createTextNode(", "));
|
||||
questionElement.appendChild(noElement);
|
||||
|
@ -657,6 +816,7 @@ function showToast(label, iconElement) {
|
|||
toastMsgElement.replaceChildren(iconElement.content.cloneNode(true));
|
||||
appendIconLabel(toastMsgElement, label);
|
||||
|
||||
<<<<<<< HEAD
|
||||
const toastElementWrapper = document.getElementById("toast-wrapper");
|
||||
if (toastElementWrapper) {
|
||||
toastElementWrapper.classList.remove('toast-animate');
|
||||
|
@ -664,6 +824,15 @@ function showToast(label, iconElement) {
|
|||
toastElementWrapper.classList.add('toast-animate');
|
||||
}, 100);
|
||||
}
|
||||
=======
|
||||
const toastElementWrapper = document.getElementById("toast-wrapper");
|
||||
if (toastElementWrapper) {
|
||||
toastElementWrapper.classList.remove("toast-animate");
|
||||
setTimeout(function () {
|
||||
toastElementWrapper.classList.add("toast-animate");
|
||||
}, 100);
|
||||
>>>>>>> 55d8ddfa (PWA: First implementation of offline mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -681,13 +850,19 @@ function handlePlayerProgressionSaveAndMarkAsReadOnCompletion(playerElement) {
|
|||
return; //If the player is not playing, we do not want to save the progression and mark as read on completion
|
||||
}
|
||||
const currentPositionInSeconds = Math.floor(playerElement.currentTime); // we do not need a precise value
|
||||
const lastKnownPositionInSeconds = parseInt(playerElement.dataset.lastPosition, 10);
|
||||
const markAsReadOnCompletion = parseFloat(playerElement.dataset.markReadOnCompletion); //completion percentage to mark as read
|
||||
const lastKnownPositionInSeconds = parseInt(
|
||||
playerElement.dataset.lastPosition,
|
||||
10,
|
||||
);
|
||||
const markAsReadOnCompletion = parseFloat(
|
||||
playerElement.dataset.markReadOnCompletion,
|
||||
); //completion percentage to mark as read
|
||||
const recordInterval = 10;
|
||||
|
||||
// we limit the number of update to only one by interval. Otherwise, we would have multiple update per seconds
|
||||
if (currentPositionInSeconds >= (lastKnownPositionInSeconds + recordInterval) ||
|
||||
currentPositionInSeconds <= (lastKnownPositionInSeconds - recordInterval)
|
||||
if (
|
||||
currentPositionInSeconds >= lastKnownPositionInSeconds + recordInterval ||
|
||||
currentPositionInSeconds <= lastKnownPositionInSeconds - recordInterval
|
||||
) {
|
||||
playerElement.dataset.lastPosition = currentPositionInSeconds.toString();
|
||||
const request = new RequestBuilder(playerElement.dataset.saveUrl);
|
||||
|
@ -697,7 +872,11 @@ function handlePlayerProgressionSaveAndMarkAsReadOnCompletion(playerElement) {
|
|||
if (markAsReadOnCompletion >= 0 && playerElement.duration > 0) {
|
||||
const completion = currentPositionInSeconds / playerElement.duration;
|
||||
if (completion >= markAsReadOnCompletion) {
|
||||
handleEntryStatus("none", document.querySelector(":is(a, button)[data-toggle-status]"), true);
|
||||
handleEntryStatus(
|
||||
"none",
|
||||
document.querySelector(":is(a, button)[data-toggle-status]"),
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -709,19 +888,23 @@ function handlePlayerProgressionSaveAndMarkAsReadOnCompletion(playerElement) {
|
|||
* @returns {boolean}
|
||||
*/
|
||||
function isPlayerPlaying(element) {
|
||||
return element &&
|
||||
return (
|
||||
element &&
|
||||
element.currentTime > 0 &&
|
||||
!element.paused &&
|
||||
!element.ended &&
|
||||
element.readyState > 2; //https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/readyState
|
||||
element.readyState > 2
|
||||
); //https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/readyState
|
||||
}
|
||||
|
||||
/**
|
||||
* handle new share entires and already shared entries
|
||||
*/
|
||||
function handleShare() {
|
||||
const link = document.querySelector(':is(a, button)[data-share-status]');
|
||||
const title = document.querySelector("body > main > section > header > h1 > a");
|
||||
const link = document.querySelector(":is(a, button)[data-share-status]");
|
||||
const title = document.querySelector(
|
||||
"body > main > section > header > h1 > a",
|
||||
);
|
||||
if (link.dataset.shareStatus === "shared") {
|
||||
checkShareAPI(title, link.href);
|
||||
}
|
||||
|
@ -747,7 +930,7 @@ function checkShareAPI(title, url) {
|
|||
try {
|
||||
navigator.share({
|
||||
title: title,
|
||||
url: url
|
||||
url: url,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
@ -772,6 +955,7 @@ function getCsrfToken() {
|
|||
* @param {Element} button
|
||||
*/
|
||||
function handleMediaControl(button) {
|
||||
<<<<<<< HEAD
|
||||
const action = button.dataset.enclosureAction;
|
||||
const value = parseFloat(button.dataset.actionValue);
|
||||
const targetEnclosureId = button.dataset.enclosureId;
|
||||
|
@ -802,4 +986,41 @@ function handleMediaControl(button) {
|
|||
break;
|
||||
}
|
||||
});
|
||||
=======
|
||||
const action = button.dataset.enclosureAction;
|
||||
const value = parseFloat(button.dataset.actionValue);
|
||||
const targetEnclosureId = button.dataset.enclosureId;
|
||||
const enclosures = document.querySelectorAll(
|
||||
`audio[data-enclosure-id="${targetEnclosureId}"],video[data-enclosure-id="${targetEnclosureId}"]`,
|
||||
);
|
||||
const speedIndicator = document.querySelectorAll(
|
||||
`span.speed-indicator[data-enclosure-id="${targetEnclosureId}"]`,
|
||||
);
|
||||
enclosures.forEach((enclosure) => {
|
||||
switch (action) {
|
||||
case "seek":
|
||||
enclosure.currentTime =
|
||||
enclosure.currentTime + value > 0 ? enclosure.currentTime + value : 0;
|
||||
break;
|
||||
case "speed":
|
||||
// I set a floor speed of 0.25 to avoid too slow speed where it gives the impression it stopped.
|
||||
// 0.25 was chosen because it will allow to get back to 1x in two "faster" click, and lower value with same property would be 0.
|
||||
enclosure.playbackRate = Math.max(0.25, enclosure.playbackRate + value);
|
||||
speedIndicator.forEach((speedI) => {
|
||||
// Two digit precision to ensure we always have the same number of characters (4) to avoid controls moving when clicking buttons because of more or less characters.
|
||||
// The trick only work on rate less than 10, but it feels an acceptable tread of considering the feature
|
||||
speedI.innerText = `${enclosure.playbackRate.toFixed(2)}x`;
|
||||
});
|
||||
break;
|
||||
case "speed-reset":
|
||||
enclosure.playbackRate = value;
|
||||
speedIndicator.forEach((speedI) => {
|
||||
// Two digit precision to ensure we always have the same number of characters (4) to avoid controls moving when clicking buttons because of more or less characters.
|
||||
// The trick only work on rate less than 10, but it feels an acceptable tread of considering the feature
|
||||
speedI.innerText = `${enclosure.playbackRate.toFixed(2)}x`;
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
>>>>>>> 55d8ddfa (PWA: First implementation of offline mode)
|
||||
}
|
||||
|
|
|
@ -1,44 +1,126 @@
|
|||
|
||||
// Incrementing OFFLINE_VERSION will kick off the install event and force
|
||||
// previously cached resources to be updated from the network.
|
||||
const OFFLINE_VERSION = 1;
|
||||
const OFFLINE_VERSION = 2;
|
||||
const CACHE_NAME = "offline";
|
||||
|
||||
const cachedPages = [
|
||||
"/unread",
|
||||
"/starred",
|
||||
"/stylesheets",
|
||||
"/app",
|
||||
"/service-worker",
|
||||
"/manifest.json",
|
||||
"/feed/icon",
|
||||
"/icon",
|
||||
];
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
event.waitUntil(
|
||||
(async () => {
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
|
||||
if (USE_CACHE) {
|
||||
await cache.addAll(["/", "/unread", "/starred", OFFLINE_URL]);
|
||||
} else {
|
||||
// Setting {cache: 'reload'} in the new request will ensure that the
|
||||
// response isn't fulfilled from the HTTP cache; i.e., it will be from
|
||||
// the network.
|
||||
await cache.add(new Request(OFFLINE_URL, { cache: "reload" }));
|
||||
})()
|
||||
}
|
||||
})(),
|
||||
);
|
||||
|
||||
// Force the waiting service worker to become the active service worker.
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
async function cacheFirstWithRefresh(request) {
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
|
||||
const fetchResponsePromise = fetch(request).then(async (networkResponse) => {
|
||||
if (!networkResponse.ok) return networkResponse;
|
||||
|
||||
const contentType = networkResponse.headers.get("Content-Type");
|
||||
if (!contentType || !contentType.includes("text/html")) {
|
||||
cache.put(request, networkResponse.clone());
|
||||
return networkResponse;
|
||||
}
|
||||
|
||||
const text = await networkResponse.clone().text();
|
||||
|
||||
const modifiedHtml = text.replace(/offline-hidden/g, "offline-visibe");
|
||||
|
||||
const clonedResponse = new Response(modifiedHtml, {
|
||||
status: networkResponse.status,
|
||||
statusText: networkResponse.statusText,
|
||||
headers: networkResponse.headers,
|
||||
});
|
||||
|
||||
cache.put(request, clonedResponse.clone());
|
||||
return networkResponse;
|
||||
});
|
||||
|
||||
try {
|
||||
return (await cache.match(request)) || (await fetchResponsePromise);
|
||||
} catch (error) {
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
return await cache.match(OFFLINE_URL);
|
||||
}
|
||||
}
|
||||
|
||||
self.addEventListener("fetch", (event) => {
|
||||
if (USE_CACHE) {
|
||||
const url = new URL(event.request.url);
|
||||
if (cachedPages.some((page) => url.pathname.startsWith(page))) {
|
||||
return event.respondWith(cacheFirstWithRefresh(event.request));
|
||||
}
|
||||
}
|
||||
|
||||
// We proxify requests through fetch() only if we are offline because it's slower.
|
||||
if (navigator.onLine === false && event.request.mode === "navigate") {
|
||||
event.respondWith(
|
||||
(async () => {
|
||||
try {
|
||||
// Always try the network first.
|
||||
const networkResponse = await fetch(event.request);
|
||||
return networkResponse;
|
||||
return await fetch(event.request);
|
||||
} catch (error) {
|
||||
// catch is only triggered if an exception is thrown, which is likely
|
||||
// due to a network error.
|
||||
// If fetch() returns a valid HTTP response with a response code in
|
||||
// the 4xx or 5xx range, the catch() will NOT be called.
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
const cachedResponse = await cache.match(OFFLINE_URL);
|
||||
return cachedResponse;
|
||||
return await cache.match(OFFLINE_URL);
|
||||
}
|
||||
})()
|
||||
})(),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
self.addEventListener("load", async (event) => {
|
||||
if (
|
||||
navigator.onLine === true &&
|
||||
event.target.location.pathname === "/unread" &&
|
||||
USE_CACHE
|
||||
) {
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
|
||||
for (let article of document.getElementsByTagName("article")) {
|
||||
const as = article.getElementsByTagName("a");
|
||||
if (as.length > 0) {
|
||||
const a = as[0];
|
||||
const href = a.href;
|
||||
cache
|
||||
.add(
|
||||
new Request(href, {
|
||||
headers: new Headers({
|
||||
"Client-Type": "service-worker",
|
||||
}),
|
||||
}),
|
||||
)
|
||||
.then(() => {
|
||||
article;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -31,7 +31,19 @@ func (h *handler) showJavascript(w http.ResponseWriter, r *http.Request) {
|
|||
contents := static.JavascriptBundles[filename]
|
||||
|
||||
if filename == "service-worker" {
|
||||
variables := fmt.Sprintf(`const OFFLINE_URL=%q;`, route.Path(h.router, "offline"))
|
||||
user, err := h.store.UserByID(request.UserID(r))
|
||||
if err != nil {
|
||||
html.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
cacheForOffline := 0
|
||||
if user.CacheForOffline {
|
||||
cacheForOffline = 1
|
||||
}
|
||||
|
||||
variables := fmt.Sprintf(`const OFFLINE_URL=%q;const USE_CACHE=%d;`, route.Path(h.router, "offline"), cacheForOffline)
|
||||
|
||||
contents = append([]byte(variables), contents...)
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue