Compare commits

...

30 Commits

Author SHA1 Message Date
Frédéric Guillot f68046cce0 docs(changelog): update release notes for version 2.2.8 2025-04-22 21:01:43 -07:00
Frédéric Guillot d33e305af9 fix(api): hide_globally categories field should be a boolean 2025-04-21 19:43:25 -07:00
Frédéric Guillot 764212f37c refactor(js): replace DomHelper methods with standalone functions 2025-04-17 18:15:08 -07:00
Tali Auster e02b65d4bc fix: deal with navigator.share exceptions
Navigator.share returns a promise that's executed in the background, but
unless we await it explicitly, we won't get the exceptions in the
try/catch block.
2025-04-17 17:07:38 -07:00
Tali Auster fe7ec25a09 chore: fix indentation 2025-04-17 17:07:38 -07:00
Tali Auster 2959a4d2bf fix: clarify share flow in UI
Prior to this commit, to share an entry, a user has to click on the
share link and then copy the URL they are redirected to. The danger is
that they may right-click and copy the share link without actually
clicking on it, and therefore share a link that, when authenticated,
shares the entry, rather than actually sharing the entry.

Here, we avoid this misinterpretation by making sharing into a POST
request and using a form rather than a link.
2025-04-17 17:07:38 -07:00
AiraNadih 6b70a7dc81 feat(api): add update_content query parameter to /entries/{entryID}/fetch-content endpoint 2025-04-17 12:41:58 -07:00
dependabot[bot] 495c6aacd9 build(deps): bump github.com/mattn/go-sqlite3 from 1.14.27 to 1.14.28
Bumps [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) from 1.14.27 to 1.14.28.
- [Release notes](https://github.com/mattn/go-sqlite3/releases)
- [Commits](https://github.com/mattn/go-sqlite3/compare/v1.14.27...v1.14.28)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-sqlite3
  dependency-version: 1.14.28
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-16 19:27:11 -07:00
Frédéric Guillot bc228d0afe ci: add documentation issue template 2025-04-12 14:29:52 -07:00
Frédéric Guillot d139d8a6ce feat(cli): add -reset-feed-next-check-at argument 2025-04-11 15:56:57 -07:00
dependabot[bot] 28d0185e79 build(deps): bump github.com/PuerkitoBio/goquery from 1.10.2 to 1.10.3
Bumps [github.com/PuerkitoBio/goquery](https://github.com/PuerkitoBio/goquery) from 1.10.2 to 1.10.3.
- [Release notes](https://github.com/PuerkitoBio/goquery/releases)
- [Commits](https://github.com/PuerkitoBio/goquery/compare/v1.10.2...v1.10.3)

---
updated-dependencies:
- dependency-name: github.com/PuerkitoBio/goquery
  dependency-version: 1.10.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-11 15:44:47 -07:00
Frédéric Guillot c87c93d85f feat(config): add SCHEDULER_ROUND_ROBIN_MAX_INTERVAL option
Add option to cap maximum refresh interval when RSS TTL, Retry-After, Cache-Control, or Expires headers specify excessively high values.
2025-04-11 15:40:32 -07:00
dependabot[bot] 0ef21e85c2 build(deps): bump github.com/prometheus/client_golang
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.21.1 to 1.22.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.21.1...v1.22.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-version: 1.22.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-08 20:53:15 -07:00
dependabot[bot] 7438d67061 build(deps): bump github.com/tdewolff/minify/v2 from 2.22.4 to 2.23.1
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.22.4 to 2.23.1.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.22.4...v2.23.1)

---
updated-dependencies:
- dependency-name: github.com/tdewolff/minify/v2
  dependency-version: 2.23.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 20:49:12 -07:00
dependabot[bot] e4399b4f9a build(deps): bump golang.org/x/image from 0.25.0 to 0.26.0
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.25.0 to 0.26.0.
- [Commits](https://github.com/golang/image/compare/v0.25.0...v0.26.0)

---
updated-dependencies:
- dependency-name: golang.org/x/image
  dependency-version: 0.26.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 20:48:54 -07:00
dependabot[bot] b4f891705a build(deps): bump golang.org/x/oauth2 from 0.28.0 to 0.29.0
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.28.0 to 0.29.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.28.0...v0.29.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-version: 0.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 20:10:31 -07:00
dependabot[bot] 3d72404ace build(deps): bump golang.org/x/net from 0.38.0 to 0.39.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.38.0 to 0.39.0.
- [Commits](https://github.com/golang/net/compare/v0.38.0...v0.39.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.39.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 19:55:08 -07:00
Matthaiks c4e92e6111 feat(locale): update Polish translation 2025-04-07 12:06:44 -07:00
Frédéric Guillot ef22e95f8b feat: implement proxy URL per feed 2025-04-06 21:05:19 -07:00
tssujt 7b344de846 feat(telegrambot): replace "Go to website" button with "Go to Miniflux" 2025-04-06 18:30:42 -07:00
Frédéric Guillot c45b51d1f8 feat: use Cache-Control max-age and Expires headers to calculate next check 2025-04-06 16:24:00 -07:00
Frédéric Guillot 0af1a6e121 refactor: avoid logging twice the feed errors in the background worker 2025-04-06 15:39:40 -07:00
Frédéric Guillot 535fd050b7 feat: add proxy rotation functionality 2025-04-06 14:59:00 -07:00
Frédéric Guillot d20e8a4e2c ci(linter): replace commitlint with a Python script 2025-04-05 20:41:34 -07:00
Qeynos 7514e8a0c1 feat(locale): update Chinese translation 2025-04-04 19:40:44 -07:00
Cthulhux ca3ede3183 Update de_DE.json
More translations
2025-04-04 19:37:53 -07:00
dependabot[bot] 038d33600a build(deps): bump github.com/coreos/go-oidc/v3 from 3.13.0 to 3.14.1
Bumps [github.com/coreos/go-oidc/v3](https://github.com/coreos/go-oidc) from 3.13.0 to 3.14.1.
- [Release notes](https://github.com/coreos/go-oidc/releases)
- [Commits](https://github.com/coreos/go-oidc/compare/v3.13.0...v3.14.1)

---
updated-dependencies:
- dependency-name: github.com/coreos/go-oidc/v3
  dependency-version: 3.14.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-03 17:34:17 -07:00
dependabot[bot] 1a3a174688 build(deps): bump github.com/mattn/go-sqlite3 from 1.14.25 to 1.14.27
Bumps [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) from 1.14.25 to 1.14.27.
- [Release notes](https://github.com/mattn/go-sqlite3/releases)
- [Commits](https://github.com/mattn/go-sqlite3/compare/v1.14.25...v1.14.27)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-sqlite3
  dependency-version: 1.14.27
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-02 19:25:34 -07:00
dependabot[bot] 6848c759b5 build(deps): bump github.com/mattn/go-sqlite3 from 1.14.24 to 1.14.25
Bumps [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) from 1.14.24 to 1.14.25.
- [Release notes](https://github.com/mattn/go-sqlite3/releases)
- [Commits](https://github.com/mattn/go-sqlite3/compare/v1.14.24...v1.14.25)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-sqlite3
  dependency-version: 1.14.25
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-01 19:46:53 -07:00
dependabot[bot] 10578f512f build(deps): bump github.com/go-webauthn/webauthn from 0.12.2 to 0.12.3
Bumps [github.com/go-webauthn/webauthn](https://github.com/go-webauthn/webauthn) from 0.12.2 to 0.12.3.
- [Release notes](https://github.com/go-webauthn/webauthn/releases)
- [Commits](https://github.com/go-webauthn/webauthn/compare/v0.12.2...v0.12.3)

---
updated-dependencies:
- dependency-name: github.com/go-webauthn/webauthn
  dependency-version: 0.12.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-01 19:46:40 -07:00
88 changed files with 1610 additions and 534 deletions
+81
View File
@@ -0,0 +1,81 @@
name: "Documentation Issue"
description: "Report issues or suggest improvements for the documentation"
title: "[Docs]: "
type: "Documentation"
labels: ["triage needed"]
body:
- type: markdown
attributes:
value: |
Thanks for helping improve the Miniflux documentation! Clear and accurate documentation helps everyone.
- type: dropdown
id: issue_type
attributes:
label: "Documentation Issue Type"
description: "What kind of documentation issue are you reporting?"
options:
- "Missing Information"
- "Incorrect Information"
- "Outdated Information"
- "Unclear Explanation"
- "Formatting/Structural Issue"
- "Typo/Grammar Error"
- "Documentation Request"
- "Other"
validations:
required: true
- type: input
id: summary
attributes:
label: "Summary"
description: "Briefly describe the documentation issue."
placeholder: "e.g., The API authentication section is outdated"
validations:
required: true
- type: input
id: location
attributes:
label: "Location"
description: "Where is the documentation you're referring to? Provide URLs, file paths, or section names."
placeholder: "e.g., README.md, docs/api.md, Installation section of the website"
validations:
required: true
- type: textarea
id: description
attributes:
label: "Detailed Description"
description: "Provide a detailed description of the issue or improvement."
placeholder: "e.g., The API authentication section doesn't mention the new token-based authentication method introduced in version 2.0.5."
validations:
required: true
- type: textarea
id: current_content
attributes:
label: "Current Content (if applicable)"
description: "What does the current documentation say?"
placeholder: "Paste the current documentation text here."
validations:
required: false
- type: textarea
id: suggested_content
attributes:
label: "Suggested Changes"
description: "If you have specific suggestions for how to improve the documentation, please provide them here."
placeholder: "e.g., Add a new section about token-based authentication with these details..."
validations:
required: false
- type: input
id: version
attributes:
label: "Version"
description: "Which version of Miniflux does this documentation issue relate to?"
placeholder: "e.g., 2.2.6, or 'all versions'"
validations:
required: false
+5 -8
View File
@@ -39,19 +39,16 @@ jobs:
run: gofmt -d -e .
commitlint:
if: github.event_name == 'pull_request'
name: Commit Linter
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
node-version: "lts/*"
- name: Install commitlint
run: |
npm install --save-dev @commitlint/config-conventional @commitlint/cli
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js
python-version: '3.13'
- name: Validate PR commits
run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose
run: python3 .github/workflows/scripts/commit-checker.py --base ${{ github.event.pull_request.base.sha }} --head ${{ github.event.pull_request.head.sha }}
@@ -0,0 +1,93 @@
import subprocess
import re
import sys
import argparse
from typing import Match
# Conventional commit pattern
CONVENTIONAL_COMMIT_PATTERN: str = r"^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9-]+\))?!?: .{1,100}"
def get_commit_message(commit_hash: str) -> str:
"""Get the commit message for a given commit hash."""
try:
result: subprocess.CompletedProcess = subprocess.run(
["git", "show", "-s", "--format=%B", commit_hash],
capture_output=True,
text=True,
check=True,
)
return result.stdout.strip()
except subprocess.CalledProcessError as e:
print(f"Error retrieving commit message: {e}")
sys.exit(1)
def check_commit_message(
message: str, pattern: str = CONVENTIONAL_COMMIT_PATTERN
) -> bool:
"""Check if commit message follows conventional commit format."""
first_line: str = message.split("\n")[0]
match: Match[str] | None = re.match(pattern, first_line)
return bool(match)
def check_commit_range(base_ref: str, head_ref: str) -> list[dict[str, str]]:
"""Check all commits in a range for compliance."""
try:
result: subprocess.CompletedProcess = subprocess.run(
["git", "log", "--format=%H", f"{base_ref}..{head_ref}"],
capture_output=True,
text=True,
check=True,
)
commit_hashes: list[str] = result.stdout.strip().split("\n")
# Filter out empty lines
commit_hashes = [hash for hash in commit_hashes if hash]
non_compliant: list[dict[str, str]] = []
for commit_hash in commit_hashes:
message: str = get_commit_message(commit_hash)
if not check_commit_message(message):
non_compliant.append(
{"hash": commit_hash, "message": message.split("\n")[0]}
)
return non_compliant
except subprocess.CalledProcessError as e:
print(f"Error checking commit range: {e}")
sys.exit(1)
def main() -> None:
parser: argparse.ArgumentParser = argparse.ArgumentParser(
description="Check conventional commit compliance"
)
parser.add_argument(
"--base", required=True, help="Base ref (starting commit, exclusive)"
)
parser.add_argument(
"--head", required=True, help="Head ref (ending commit, inclusive)"
)
args: argparse.Namespace = parser.parse_args()
non_compliant: list[dict[str, str]] = check_commit_range(args.base, args.head)
if non_compliant:
print("The following commits do not follow the conventional commit format:")
for commit in non_compliant:
print(f"- {commit['hash'][:8]}: {commit['message']}")
print("\nPlease ensure your commit messages follow the format:")
print("type(scope): subject")
print(
"\nWhere type is one of: build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test"
)
sys.exit(1)
else:
print("All commits follow the conventional commit format!")
sys.exit(0)
if __name__ == "__main__":
main()
+30
View File
@@ -1,3 +1,33 @@
Version 2.2.8 (April 22, 2025)
------------------------------
* refactor(js): replace `DomHelper` methods with standalone functions
* refactor: avoid logging twice the feed errors in the background worker
* fix(api): `hide_globally` categories field should be a boolean
* fix(ui): add missing `await` when calling `navigator.share()` method
* fix(ui): replace share link with a form button for better accessibility
* feat(telegrambot): replace "Go to website" button with "Go to Miniflux"
* feat(locale): update Polish translation
* feat(locale): update German translation
* feat(locale): update Chinese translation
* feat(config): add `SCHEDULER_ROUND_ROBIN_MAX_INTERVAL` option
* feat(cli): add `-reset-feed-next-check-at` argument
* feat(api): add `update_content` query parameter to `/entries/{entryID}/fetch-content` endpoint
* feat: use `Cache-Control` max-age and `Expires` headers to calculate next check
* feat: implement proxy URL per feed
* feat: add proxy rotation functionality
* ci(linter): replace commitlint with a Python script
* ci: add documentation issue template
* build(deps): bump `golang.org/x/oauth2` from `0.28.0` to `0.29.0`
* build(deps): bump `golang.org/x/net` from `0.38.0` to `0.39.0`
* build(deps): bump `golang.org/x/image` from `0.25.0` to `0.26.0`
* build(deps): bump `github.com/tdewolff/minify/v2` from `2.22.4` to `2.23.1`
* build(deps): bump `github.com/PuerkitoBio/goquery` from `1.10.2` to `1.10.3`
* build(deps): bump `github.com/prometheus/client_golang`
* build(deps): bump `github.com/mattn/go-sqlite3` from `1.14.24` to `1.14.28`
* build(deps): bump `github.com/go-webauthn/webauthn` from `0.12.2` to `0.12.3`
* build(deps): bump `github.com/coreos/go-oidc/v3` from `3.13.0` to `3.14.1`
Version 2.2.7 (April 1, 2025)
-----------------------------
+1 -1
View File
@@ -144,7 +144,7 @@ integration-test:
ADMIN_PASSWORD=test123 \
CREATE_ADMIN=1 \
RUN_MIGRATIONS=1 \
DEBUG=1 \
LOG_LEVEL=debug \
./miniflux-test >/tmp/miniflux.log 2>&1 & echo "$$!" > "/tmp/miniflux.pid"
while ! nc -z localhost 8080; do sleep 1; done
+35 -4
View File
@@ -239,8 +239,8 @@ func (c *Client) Categories() (Categories, error) {
// CreateCategory creates a new category.
func (c *Client) CreateCategory(title string) (*Category, error) {
body, err := c.request.Post("/v1/categories", map[string]interface{}{
"title": title,
body, err := c.request.Post("/v1/categories", &CategoryCreationRequest{
Title: title,
})
if err != nil {
return nil, err
@@ -255,10 +255,25 @@ func (c *Client) CreateCategory(title string) (*Category, error) {
return category, nil
}
// CreateCategoryWithOptions creates a new category with options.
func (c *Client) CreateCategoryWithOptions(createRequest *CategoryCreationRequest) (*Category, error) {
body, err := c.request.Post("/v1/categories", createRequest)
if err != nil {
return nil, err
}
defer body.Close()
var category *Category
if err := json.NewDecoder(body).Decode(&category); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return category, nil
}
// UpdateCategory updates a category.
func (c *Client) UpdateCategory(categoryID int64, title string) (*Category, error) {
body, err := c.request.Put(fmt.Sprintf("/v1/categories/%d", categoryID), map[string]interface{}{
"title": title,
body, err := c.request.Put(fmt.Sprintf("/v1/categories/%d", categoryID), &CategoryModificationRequest{
Title: SetOptionalField(title),
})
if err != nil {
return nil, err
@@ -273,6 +288,22 @@ func (c *Client) UpdateCategory(categoryID int64, title string) (*Category, erro
return category, nil
}
// UpdateCategoryWithOptions updates a category with options.
func (c *Client) UpdateCategoryWithOptions(categoryID int64, categoryChanges *CategoryModificationRequest) (*Category, error) {
body, err := c.request.Put(fmt.Sprintf("/v1/categories/%d", categoryID), categoryChanges)
if err != nil {
return nil, err
}
defer body.Close()
var category *Category
if err := json.NewDecoder(body).Decode(&category); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return category, nil
}
// MarkCategoryAsRead marks all unread entries in a category as read.
func (c *Client) MarkCategoryAsRead(categoryID int64) error {
_, err := c.request.Put(fmt.Sprintf("/v1/categories/%d/mark-all-as-read", categoryID), nil)
+21 -3
View File
@@ -97,9 +97,12 @@ type Users []User
// Category represents a feed category.
type Category struct {
ID int64 `json:"id,omitempty"`
Title string `json:"title,omitempty"`
UserID int64 `json:"user_id,omitempty"`
ID int64 `json:"id"`
Title string `json:"title"`
UserID int64 `json:"user_id,omitempty"`
HideGlobally bool `json:"hide_globally,omitempty"`
FeedCount *int `json:"feed_count,omitempty"`
TotalUnread *int `json:"total_unread,omitempty"`
}
func (c Category) String() string {
@@ -109,6 +112,18 @@ func (c Category) String() string {
// Categories represents a list of categories.
type Categories []*Category
// CategoryCreationRequest represents the request to create a category.
type CategoryCreationRequest struct {
Title string `json:"title"`
HideGlobally bool `json:"hide_globally"`
}
// CategoryModificationRequest represents the request to update a category.
type CategoryModificationRequest struct {
Title *string `json:"title"`
HideGlobally *bool `json:"hide_globally"`
}
// Subscription represents a feed subscription.
type Subscription struct {
Title string `json:"title"`
@@ -151,6 +166,7 @@ type Feed struct {
Category *Category `json:"category,omitempty"`
HideGlobally bool `json:"hide_globally"`
DisableHTTP2 bool `json:"disable_http2"`
ProxyURL string `json:"proxy_url"`
}
// FeedCreationRequest represents the request to create a feed.
@@ -172,6 +188,7 @@ type FeedCreationRequest struct {
KeeplistRules string `json:"keeplist_rules"`
HideGlobally bool `json:"hide_globally"`
DisableHTTP2 bool `json:"disable_http2"`
ProxyURL string `json:"proxy_url"`
}
// FeedModificationRequest represents the request to update a feed.
@@ -195,6 +212,7 @@ type FeedModificationRequest struct {
FetchViaProxy *bool `json:"fetch_via_proxy"`
HideGlobally *bool `json:"hide_globally"`
DisableHTTP2 *bool `json:"disable_http2"`
ProxyURL *string `json:"proxy_url"`
}
// FeedIcon represents the feed icon.
+17 -18
View File
@@ -3,24 +3,24 @@ module miniflux.app/v2
// +heroku goVersion go1.23
require (
github.com/PuerkitoBio/goquery v1.10.2
github.com/PuerkitoBio/goquery v1.10.3
github.com/andybalholm/brotli v1.1.1
github.com/coreos/go-oidc/v3 v3.13.0
github.com/go-webauthn/webauthn v0.12.2
github.com/coreos/go-oidc/v3 v3.14.1
github.com/go-webauthn/webauthn v0.12.3
github.com/gorilla/mux v1.8.1
github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.24
github.com/prometheus/client_golang v1.21.1
github.com/tdewolff/minify/v2 v2.22.4
golang.org/x/crypto v0.36.0
golang.org/x/image v0.25.0
golang.org/x/net v0.38.0
golang.org/x/oauth2 v0.28.0
golang.org/x/term v0.30.0
github.com/mattn/go-sqlite3 v1.14.28
github.com/prometheus/client_golang v1.22.0
github.com/tdewolff/minify/v2 v2.23.1
golang.org/x/crypto v0.37.0
golang.org/x/image v0.26.0
golang.org/x/net v0.39.0
golang.org/x/oauth2 v0.29.0
golang.org/x/term v0.31.0
)
require (
github.com/go-webauthn/x v0.1.19 // indirect
github.com/go-webauthn/x v0.1.20 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/google/go-tpm v0.9.3 // indirect
)
@@ -29,20 +29,19 @@ require (
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/tdewolff/parse/v2 v2.7.21 // indirect
github.com/tdewolff/parse/v2 v2.7.23 // indirect
github.com/x448/float16 v0.8.4 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
google.golang.org/protobuf v1.36.1 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
)
go 1.23.0
+38 -37
View File
@@ -1,5 +1,5 @@
github.com/PuerkitoBio/goquery v1.10.2 h1:7fh2BdHcG6VFZsK7toXBT/Bh1z5Wmy8Q9MV9HqT2AM8=
github.com/PuerkitoBio/goquery v1.10.2/go.mod h1:0guWGjcLu9AYC7C1GHnpysHy056u9aEkUHwhdnePMCU=
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
@@ -8,44 +8,45 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-oidc/v3 v3.13.0 h1:M66zd0pcc5VxvBNM4pB331Wrsanby+QomQYjN8HamW8=
github.com/coreos/go-oidc/v3 v3.13.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU=
github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-webauthn/webauthn v0.12.2 h1:yLaNPgBUEXDQtWnOjhsGhMMCEWbXwjg/aNkC8riJQI8=
github.com/go-webauthn/webauthn v0.12.2/go.mod h1:Q8SZPPj4sZ469fNTcQXxRpzJOdb30jQrn/36FX8jilA=
github.com/go-webauthn/x v0.1.19 h1:IUfdHiBRoTdujpBA/14qbrMXQ3LGzYe/PRGWdZcmudg=
github.com/go-webauthn/x v0.1.19/go.mod h1:C5arLuTQ3pVHKPw89v7CDGnqAZSZJj+4Jnr40dsn7tk=
github.com/go-webauthn/webauthn v0.12.3 h1:hHQl1xkUuabUU9uS+ISNCMLs9z50p9mDUZI/FmkayNE=
github.com/go-webauthn/webauthn v0.12.3/go.mod h1:4JRe8Z3W7HIw8NGEWn2fnUwecoDzkkeach/NnvhkqGY=
github.com/go-webauthn/x v0.1.20 h1:brEBDqfiPtNNCdS/peu8gARtq8fIPsHz0VzpPjGvgiw=
github.com/go-webauthn/x v0.1.20/go.mod h1:n/gAc8ssZJGATM0qThE+W+vfgXiMedsWi3wf/C4lld0=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc=
github.com/google/go-tpm v0.9.3/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
@@ -54,10 +55,10 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tdewolff/minify/v2 v2.22.4 h1:0/8K2fheOuYr5B4e5oCE1hGBVX6DQHLP0EGzdsDlYeg=
github.com/tdewolff/minify/v2 v2.22.4/go.mod h1:K/R8TT7aivpcU8QCNUU1UdR6etfnFPr7L11TO/X7shk=
github.com/tdewolff/parse/v2 v2.7.21 h1:OCuPFtGr4mXdnfKikQlUb0n654ROJANhBqCk+wioJ/A=
github.com/tdewolff/parse/v2 v2.7.21/go.mod h1:I7TXO37t3aSG9SlPUBefAhgIF8nt7yYUwVGgETIoBcA=
github.com/tdewolff/minify/v2 v2.23.1 h1:r6sKQrumHzskWZRdhiRa+pZhn7CdBMojACNP9fuKpXQ=
github.com/tdewolff/minify/v2 v2.23.1/go.mod h1:RkUGjklq6uIsBoOdzY3ll35HKKQ2aFqLQhnanBHhDyU=
github.com/tdewolff/parse/v2 v2.7.23 h1:sCW2PNTCM1yVldh5YK/8wrpRI9rSbloUZWjAydlN2IA=
github.com/tdewolff/parse/v2 v2.7.23/go.mod h1:I7TXO37t3aSG9SlPUBefAhgIF8nt7yYUwVGgETIoBcA=
github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE=
github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
@@ -71,10 +72,10 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY=
golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
@@ -89,10 +90,10 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -111,8 +112,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -122,8 +123,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@@ -133,8 +134,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
@@ -142,7 +143,7 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+132 -1
View File
@@ -824,6 +824,10 @@ func TestCreateCategoryEndpoint(t *testing.T) {
if category.Title != categoryName {
t.Errorf(`Invalid title, got "%v" instead of "%v"`, category.Title, categoryName)
}
if category.HideGlobally {
t.Errorf(`Invalid hide globally value, got "%v"`, category.HideGlobally)
}
}
func TestCreateCategoryWithEmptyTitle(t *testing.T) {
@@ -865,7 +869,49 @@ func TestCannotCreateDuplicatedCategory(t *testing.T) {
}
}
func TestUpdateCatgoryEndpoint(t *testing.T) {
func TestCreateCategoryWithOptions(t *testing.T) {
testConfig := newIntegrationTestConfig()
if !testConfig.isConfigured() {
t.Skip(skipIntegrationTestsMessage)
}
adminClient := miniflux.NewClient(testConfig.testBaseURL, testConfig.testAdminUsername, testConfig.testAdminPassword)
regularTestUser, err := adminClient.CreateUser(testConfig.genRandomUsername(), testConfig.testRegularPassword, false)
if err != nil {
t.Fatal(err)
}
defer adminClient.DeleteUser(regularTestUser.ID)
regularUserClient := miniflux.NewClient(testConfig.testBaseURL, regularTestUser.Username, testConfig.testRegularPassword)
newCategory, err := regularUserClient.CreateCategoryWithOptions(&miniflux.CategoryCreationRequest{
Title: "My category",
HideGlobally: true,
})
if err != nil {
t.Fatalf(`Creating a category with options should not raise an error: %v`, err)
}
categories, err := regularUserClient.Categories()
if err != nil {
t.Fatal(err)
}
for _, category := range categories {
if category.ID == newCategory.ID {
if category.Title != newCategory.Title {
t.Errorf(`Invalid title, got %q instead of %q`, category.Title, newCategory.Title)
}
if category.HideGlobally != true {
t.Errorf(`Invalid hide globally value, got "%v"`, category.HideGlobally)
}
break
}
}
}
func TestUpdateCategoryEndpoint(t *testing.T) {
testConfig := newIntegrationTestConfig()
if !testConfig.isConfigured() {
t.Skip(skipIntegrationTestsMessage)
@@ -903,6 +949,91 @@ func TestUpdateCatgoryEndpoint(t *testing.T) {
if updatedCategory.Title != "new title" {
t.Errorf(`Invalid title, got "%v" instead of "%v"`, updatedCategory.Title, "new title")
}
if updatedCategory.HideGlobally {
t.Errorf(`Invalid hide globally value, got "%v"`, updatedCategory.HideGlobally)
}
}
func TestUpdateCategoryWithOptions(t *testing.T) {
testConfig := newIntegrationTestConfig()
if !testConfig.isConfigured() {
t.Skip(skipIntegrationTestsMessage)
}
adminClient := miniflux.NewClient(testConfig.testBaseURL, testConfig.testAdminUsername, testConfig.testAdminPassword)
regularTestUser, err := adminClient.CreateUser(testConfig.genRandomUsername(), testConfig.testRegularPassword, false)
if err != nil {
t.Fatal(err)
}
defer adminClient.DeleteUser(regularTestUser.ID)
regularUserClient := miniflux.NewClient(testConfig.testBaseURL, regularTestUser.Username, testConfig.testRegularPassword)
newCategory, err := regularUserClient.CreateCategoryWithOptions(&miniflux.CategoryCreationRequest{
Title: "My category",
})
if err != nil {
t.Fatalf(`Creating a category with options should not raise an error: %v`, err)
}
updatedCategory, err := regularUserClient.UpdateCategoryWithOptions(newCategory.ID, &miniflux.CategoryModificationRequest{
Title: miniflux.SetOptionalField("new title"),
})
if err != nil {
t.Fatal(err)
}
if updatedCategory.ID != newCategory.ID {
t.Errorf(`Invalid categoryID, got "%v"`, updatedCategory.ID)
}
if updatedCategory.Title != "new title" {
t.Errorf(`Invalid title, got "%v" instead of "%v"`, updatedCategory.Title, "new title")
}
if updatedCategory.HideGlobally {
t.Errorf(`Invalid hide globally value, got "%v"`, updatedCategory.HideGlobally)
}
updatedCategory, err = regularUserClient.UpdateCategoryWithOptions(newCategory.ID, &miniflux.CategoryModificationRequest{
HideGlobally: miniflux.SetOptionalField(true),
})
if err != nil {
t.Fatal(err)
}
if updatedCategory.ID != newCategory.ID {
t.Errorf(`Invalid categoryID, got "%v"`, updatedCategory.ID)
}
if updatedCategory.Title != "new title" {
t.Errorf(`Invalid title, got "%v" instead of "%v"`, updatedCategory.Title, "new title")
}
if !updatedCategory.HideGlobally {
t.Errorf(`Invalid hide globally value, got "%v"`, updatedCategory.HideGlobally)
}
updatedCategory, err = regularUserClient.UpdateCategoryWithOptions(newCategory.ID, &miniflux.CategoryModificationRequest{
HideGlobally: miniflux.SetOptionalField(false),
})
if err != nil {
t.Fatal(err)
}
if updatedCategory.ID != newCategory.ID {
t.Errorf(`Invalid categoryID, got %q`, updatedCategory.ID)
}
if updatedCategory.Title != "new title" {
t.Errorf(`Invalid title, got %q instead of %q`, updatedCategory.Title, "new title")
}
if updatedCategory.HideGlobally {
t.Errorf(`Invalid hide globally value, got "%v"`, updatedCategory.HideGlobally)
}
}
func TestUpdateInexistingCategory(t *testing.T) {
+10 -10
View File
@@ -19,18 +19,18 @@ import (
func (h *handler) createCategory(w http.ResponseWriter, r *http.Request) {
userID := request.UserID(r)
var categoryRequest model.CategoryRequest
if err := json_parser.NewDecoder(r.Body).Decode(&categoryRequest); err != nil {
var categoryCreationRequest model.CategoryCreationRequest
if err := json_parser.NewDecoder(r.Body).Decode(&categoryCreationRequest); err != nil {
json.BadRequest(w, r, err)
return
}
if validationErr := validator.ValidateCategoryCreation(h.store, userID, &categoryRequest); validationErr != nil {
if validationErr := validator.ValidateCategoryCreation(h.store, userID, &categoryCreationRequest); validationErr != nil {
json.BadRequest(w, r, validationErr.Error())
return
}
category, err := h.store.CreateCategory(userID, &categoryRequest)
category, err := h.store.CreateCategory(userID, &categoryCreationRequest)
if err != nil {
json.ServerError(w, r, err)
return
@@ -54,20 +54,20 @@ func (h *handler) updateCategory(w http.ResponseWriter, r *http.Request) {
return
}
var categoryRequest model.CategoryRequest
if err := json_parser.NewDecoder(r.Body).Decode(&categoryRequest); err != nil {
var categoryModificationRequest model.CategoryModificationRequest
if err := json_parser.NewDecoder(r.Body).Decode(&categoryModificationRequest); err != nil {
json.BadRequest(w, r, err)
return
}
if validationErr := validator.ValidateCategoryModification(h.store, userID, category.ID, &categoryRequest); validationErr != nil {
if validationErr := validator.ValidateCategoryModification(h.store, userID, category.ID, &categoryModificationRequest); validationErr != nil {
json.BadRequest(w, r, validationErr.Error())
return
}
categoryRequest.Patch(category)
err = h.store.UpdateCategory(category)
if err != nil {
categoryModificationRequest.Patch(category)
if err := h.store.UpdateCategory(category); err != nil {
json.ServerError(w, r, err)
return
}
+12
View File
@@ -331,6 +331,18 @@ func (h *handler) fetchContent(w http.ResponseWriter, r *http.Request) {
return
}
shouldUpdateContent := request.QueryBoolParam(r, "update_content", false)
if shouldUpdateContent {
if err := h.store.UpdateEntryTitleAndContent(entry); err != nil {
json.ServerError(w, r, err)
return
}
json.OK(w, r, map[string]interface{}{"content": mediaproxy.RewriteDocumentWithRelativeProxyURL(h.router, entry.Content), "reading_time": entry.ReadingTime})
return
}
json.OK(w, r, map[string]string{"content": entry.Content})
}
+5 -2
View File
@@ -11,6 +11,7 @@ import (
"miniflux.app/v2/internal/http/request"
"miniflux.app/v2/internal/http/response/json"
"miniflux.app/v2/internal/model"
"miniflux.app/v2/internal/proxyrotator"
"miniflux.app/v2/internal/reader/fetcher"
"miniflux.app/v2/internal/reader/subscription"
"miniflux.app/v2/internal/validator"
@@ -36,11 +37,13 @@ func (h *handler) discoverSubscriptions(w http.ResponseWriter, r *http.Request)
requestBuilder := fetcher.NewRequestBuilder()
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
requestBuilder.WithProxyRotator(proxyrotator.ProxyRotatorInstance)
requestBuilder.WithCustomFeedProxyURL(subscriptionDiscoveryRequest.ProxyURL)
requestBuilder.WithCustomApplicationProxyURL(config.Opts.HTTPClientProxyURL())
requestBuilder.UseCustomApplicationProxyURL(subscriptionDiscoveryRequest.FetchViaProxy)
requestBuilder.WithUserAgent(subscriptionDiscoveryRequest.UserAgent, config.Opts.HTTPClientUserAgent())
requestBuilder.WithCookie(subscriptionDiscoveryRequest.Cookie)
requestBuilder.WithUsernameAndPassword(subscriptionDiscoveryRequest.Username, subscriptionDiscoveryRequest.Password)
requestBuilder.UseProxy(subscriptionDiscoveryRequest.FetchViaProxy)
requestBuilder.IgnoreTLSErrors(subscriptionDiscoveryRequest.AllowSelfSignedCertificates)
requestBuilder.DisableHTTP2(subscriptionDiscoveryRequest.DisableHTTP2)
+51 -30
View File
@@ -13,46 +13,49 @@ import (
"miniflux.app/v2/internal/config"
"miniflux.app/v2/internal/database"
"miniflux.app/v2/internal/proxyrotator"
"miniflux.app/v2/internal/storage"
"miniflux.app/v2/internal/ui/static"
"miniflux.app/v2/internal/version"
)
const (
flagInfoHelp = "Show build information"
flagVersionHelp = "Show application version"
flagMigrateHelp = "Run SQL migrations"
flagFlushSessionsHelp = "Flush all sessions (disconnect users)"
flagCreateAdminHelp = "Create an admin user from an interactive terminal"
flagResetPasswordHelp = "Reset user password"
flagResetFeedErrorsHelp = "Clear all feed errors for all users"
flagDebugModeHelp = "Show debug logs"
flagConfigFileHelp = "Load configuration file"
flagConfigDumpHelp = "Print parsed configuration values"
flagHealthCheckHelp = `Perform a health check on the given endpoint (the value "auto" try to guess the health check endpoint).`
flagRefreshFeedsHelp = "Refresh a batch of feeds and exit"
flagRunCleanupTasksHelp = "Run cleanup tasks (delete old sessions and archives old entries)"
flagExportUserFeedsHelp = "Export user feeds (provide the username as argument)"
flagInfoHelp = "Show build information"
flagVersionHelp = "Show application version"
flagMigrateHelp = "Run SQL migrations"
flagFlushSessionsHelp = "Flush all sessions (disconnect users)"
flagCreateAdminHelp = "Create an admin user from an interactive terminal"
flagResetPasswordHelp = "Reset user password"
flagResetFeedErrorsHelp = "Clear all feed errors for all users"
flagDebugModeHelp = "Show debug logs"
flagConfigFileHelp = "Load configuration file"
flagConfigDumpHelp = "Print parsed configuration values"
flagHealthCheckHelp = `Perform a health check on the given endpoint (the value "auto" try to guess the health check endpoint).`
flagRefreshFeedsHelp = "Refresh a batch of feeds and exit"
flagRunCleanupTasksHelp = "Run cleanup tasks (delete old sessions and archives old entries)"
flagExportUserFeedsHelp = "Export user feeds (provide the username as argument)"
flagResetNextCheckAtHelp = "Reset the next check time for all feeds"
)
// Parse parses command line arguments.
func Parse() {
var (
err error
flagInfo bool
flagVersion bool
flagMigrate bool
flagFlushSessions bool
flagCreateAdmin bool
flagResetPassword bool
flagResetFeedErrors bool
flagDebugMode bool
flagConfigFile string
flagConfigDump bool
flagHealthCheck string
flagRefreshFeeds bool
flagRunCleanupTasks bool
flagExportUserFeeds string
err error
flagInfo bool
flagVersion bool
flagMigrate bool
flagFlushSessions bool
flagCreateAdmin bool
flagResetPassword bool
flagResetFeedErrors bool
flagResetFeedNextCheckAt bool
flagDebugMode bool
flagConfigFile string
flagConfigDump bool
flagHealthCheck string
flagRefreshFeeds bool
flagRunCleanupTasks bool
flagExportUserFeeds string
)
flag.BoolVar(&flagInfo, "info", false, flagInfoHelp)
@@ -64,6 +67,7 @@ func Parse() {
flag.BoolVar(&flagCreateAdmin, "create-admin", false, flagCreateAdminHelp)
flag.BoolVar(&flagResetPassword, "reset-password", false, flagResetPasswordHelp)
flag.BoolVar(&flagResetFeedErrors, "reset-feed-errors", false, flagResetFeedErrorsHelp)
flag.BoolVar(&flagResetFeedNextCheckAt, "reset-feed-next-check-at", false, flagResetNextCheckAtHelp)
flag.BoolVar(&flagDebugMode, "debug", false, flagDebugModeHelp)
flag.StringVar(&flagConfigFile, "config-file", "", flagConfigFileHelp)
flag.StringVar(&flagConfigFile, "c", "", flagConfigFileHelp)
@@ -189,7 +193,16 @@ func Parse() {
}
if flagResetFeedErrors {
store.ResetFeedErrors()
if err := store.ResetFeedErrors(); err != nil {
printErrorAndExit(err)
}
return
}
if flagResetFeedNextCheckAt {
if err := store.ResetNextCheckAt(); err != nil {
printErrorAndExit(err)
}
return
}
@@ -228,6 +241,14 @@ func Parse() {
createAdminUserFromEnvironmentVariables(store)
}
if config.Opts.HasHTTPClientProxiesConfigured() {
slog.Info("Initializing proxy rotation", slog.Int("proxies_count", len(config.Opts.HTTPClientProxies())))
proxyrotator.ProxyRotatorInstance, err = proxyrotator.NewProxyRotator(config.Opts.HTTPClientProxies())
if err != nil {
printErrorAndExit(fmt.Errorf("unable to initialize proxy rotator: %v", err))
}
}
if flagRefreshFeeds {
refreshFeeds(store)
return
+103
View File
@@ -1028,6 +1028,41 @@ func TestSchedulerRoundRobin(t *testing.T) {
}
}
func TestDefaultSchedulerRoundRobinMaxIntervalValue(t *testing.T) {
os.Clearenv()
parser := NewParser()
opts, err := parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}
expected := defaultSchedulerRoundRobinMaxInterval
result := opts.SchedulerRoundRobinMaxInterval()
if result != expected {
t.Fatalf(`Unexpected SCHEDULER_ROUND_ROBIN_MAX_INTERVAL value, got %v instead of %v`, result, expected)
}
}
func TestSchedulerRoundRobinMaxInterval(t *testing.T) {
os.Clearenv()
os.Setenv("SCHEDULER_ROUND_ROBIN_MAX_INTERVAL", "150")
parser := NewParser()
opts, err := parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}
expected := 150
result := opts.SchedulerRoundRobinMaxInterval()
if result != expected {
t.Fatalf(`Unexpected SCHEDULER_ROUND_ROBIN_MAX_INTERVAL value, got %v instead of %v`, result, expected)
}
}
func TestPollingParsingErrorLimit(t *testing.T) {
os.Clearenv()
os.Setenv("POLLING_PARSING_ERROR_LIMIT", "100")
@@ -2186,3 +2221,71 @@ func TestParseConfigDumpOutput(t *testing.T) {
t.Fatal(err)
}
}
func TestHTTPClientProxies(t *testing.T) {
os.Clearenv()
os.Setenv("HTTP_CLIENT_PROXIES", "http://proxy1.example.com,http://proxy2.example.com")
parser := NewParser()
opts, err := parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}
expected := []string{"http://proxy1.example.com", "http://proxy2.example.com"}
result := opts.HTTPClientProxies()
if len(expected) != len(result) {
t.Fatalf(`Unexpected HTTP_CLIENT_PROXIES value, got %v instead of %v`, result, expected)
}
for i, proxy := range expected {
if result[i] != proxy {
t.Fatalf(`Unexpected HTTP_CLIENT_PROXIES value at index %d, got %q instead of %q`, i, result[i], proxy)
}
}
}
func TestDefaultHTTPClientProxiesValue(t *testing.T) {
os.Clearenv()
parser := NewParser()
opts, err := parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}
expected := []string{}
result := opts.HTTPClientProxies()
if len(expected) != len(result) {
t.Fatalf(`Unexpected default HTTP_CLIENT_PROXIES value, got %v instead of %v`, result, expected)
}
}
func TestHTTPClientProxy(t *testing.T) {
os.Clearenv()
os.Setenv("HTTP_CLIENT_PROXY", "http://proxy.example.com")
parser := NewParser()
opts, err := parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}
expected := "http://proxy.example.com"
if opts.HTTPClientProxyURL() == nil || opts.HTTPClientProxyURL().String() != expected {
t.Fatalf(`Unexpected HTTP_CLIENT_PROXY value, got %v instead of %v`, opts.HTTPClientProxyURL(), expected)
}
}
func TestInvalidHTTPClientProxy(t *testing.T) {
os.Clearenv()
os.Setenv("HTTP_CLIENT_PROXY", "sche|me://invalid-proxy-url")
parser := NewParser()
_, err := parser.ParseEnvironmentVariables()
if err == nil {
t.Fatalf(`Expected error for invalid HTTP_CLIENT_PROXY value, but got none`)
}
}
+61 -12
View File
@@ -5,6 +5,7 @@ package config // import "miniflux.app/v2/internal/config"
import (
"fmt"
"net/url"
"sort"
"strings"
"time"
@@ -36,6 +37,7 @@ const (
defaultSchedulerEntryFrequencyMaxInterval = 24 * 60
defaultSchedulerEntryFrequencyFactor = 1
defaultSchedulerRoundRobinMinInterval = 60
defaultSchedulerRoundRobinMaxInterval = 1440
defaultPollingParsingErrorLimit = 3
defaultRunMigrations = false
defaultDatabaseURL = "user=postgres password=postgres dbname=miniflux2 sslmode=disable"
@@ -136,6 +138,7 @@ type Options struct {
schedulerEntryFrequencyMaxInterval int
schedulerEntryFrequencyFactor int
schedulerRoundRobinMinInterval int
schedulerRoundRobinMaxInterval int
pollingParsingErrorLimit int
workerPoolSize int
createAdmin bool
@@ -163,7 +166,8 @@ type Options struct {
pocketConsumerKey string
httpClientTimeout int
httpClientMaxBodySize int64
httpClientProxy string
httpClientProxyURL *url.URL
httpClientProxies []string
httpClientUserAgent string
httpServerTimeout int
authProxyHeader string
@@ -218,6 +222,7 @@ func NewOptions() *Options {
schedulerEntryFrequencyMaxInterval: defaultSchedulerEntryFrequencyMaxInterval,
schedulerEntryFrequencyFactor: defaultSchedulerEntryFrequencyFactor,
schedulerRoundRobinMinInterval: defaultSchedulerRoundRobinMinInterval,
schedulerRoundRobinMaxInterval: defaultSchedulerRoundRobinMaxInterval,
pollingParsingErrorLimit: defaultPollingParsingErrorLimit,
workerPoolSize: defaultWorkerPoolSize,
createAdmin: defaultCreateAdmin,
@@ -243,7 +248,8 @@ func NewOptions() *Options {
pocketConsumerKey: defaultPocketConsumerKey,
httpClientTimeout: defaultHTTPClientTimeout,
httpClientMaxBodySize: defaultHTTPClientMaxBodySize * 1024 * 1024,
httpClientProxy: defaultHTTPClientProxy,
httpClientProxyURL: nil,
httpClientProxies: []string{},
httpClientUserAgent: defaultHTTPClientUserAgent,
httpServerTimeout: defaultHTTPServerTimeout,
authProxyHeader: defaultAuthProxyHeader,
@@ -430,6 +436,10 @@ func (o *Options) SchedulerRoundRobinMinInterval() int {
return o.schedulerRoundRobinMinInterval
}
func (o *Options) SchedulerRoundRobinMaxInterval() int {
return o.schedulerRoundRobinMaxInterval
}
// PollingParsingErrorLimit returns the limit of errors when to stop polling.
func (o *Options) PollingParsingErrorLimit() int {
return o.pollingParsingErrorLimit
@@ -587,9 +597,24 @@ func (o *Options) HTTPClientMaxBodySize() int64 {
return o.httpClientMaxBodySize
}
// HTTPClientProxy returns the proxy URL for HTTP client.
func (o *Options) HTTPClientProxy() string {
return o.httpClientProxy
// HTTPClientProxyURL returns the client HTTP proxy URL if configured.
func (o *Options) HTTPClientProxyURL() *url.URL {
return o.httpClientProxyURL
}
// HasHTTPClientProxyURLConfigured returns true if the client HTTP proxy URL if configured.
func (o *Options) HasHTTPClientProxyURLConfigured() bool {
return o.httpClientProxyURL != nil
}
// HTTPClientProxies returns the list of proxies.
func (o *Options) HTTPClientProxies() []string {
return o.httpClientProxies
}
// HTTPClientProxiesString returns true if the list of rotating proxies are configured.
func (o *Options) HasHTTPClientProxiesConfigured() bool {
return len(o.httpClientProxies) > 0
}
// HTTPServerTimeout returns the time limit in seconds before the HTTP server cancel the request.
@@ -597,11 +622,6 @@ func (o *Options) HTTPServerTimeout() int {
return o.httpServerTimeout
}
// HasHTTPClientProxyConfigured returns true if the HTTP proxy is configured.
func (o *Options) HasHTTPClientProxyConfigured() bool {
return o.httpClientProxy != ""
}
// AuthProxyHeader returns an HTTP header name that contains username for
// authentication using auth proxy.
func (o *Options) AuthProxyHeader() string {
@@ -664,6 +684,33 @@ func (o *Options) FilterEntryMaxAgeDays() int {
// SortedOptions returns options as a list of key value pairs, sorted by keys.
func (o *Options) SortedOptions(redactSecret bool) []*Option {
var clientProxyURLRedacted string
if o.httpClientProxyURL != nil {
if redactSecret {
clientProxyURLRedacted = o.httpClientProxyURL.Redacted()
} else {
clientProxyURLRedacted = o.httpClientProxyURL.String()
}
}
var clientProxyURLsRedacted string
if len(o.httpClientProxies) > 0 {
if redactSecret {
var proxyURLs []string
for range o.httpClientProxies {
proxyURLs = append(proxyURLs, "<redacted>")
}
clientProxyURLsRedacted = strings.Join(proxyURLs, ",")
} else {
clientProxyURLsRedacted = strings.Join(o.httpClientProxies, ",")
}
}
var mediaProxyPrivateKeyValue string
if len(o.mediaProxyPrivateKey) > 0 {
mediaProxyPrivateKeyValue = "<binary-data>"
}
var keyValues = map[string]interface{}{
"ADMIN_PASSWORD": redactSecretValue(o.adminPassword, redactSecret),
"ADMIN_USERNAME": o.adminUsername,
@@ -694,7 +741,8 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option {
"FETCH_BILIBILI_WATCH_TIME": o.fetchBilibiliWatchTime,
"HTTPS": o.HTTPS,
"HTTP_CLIENT_MAX_BODY_SIZE": o.httpClientMaxBodySize,
"HTTP_CLIENT_PROXY": o.httpClientProxy,
"HTTP_CLIENT_PROXIES": clientProxyURLsRedacted,
"HTTP_CLIENT_PROXY": clientProxyURLRedacted,
"HTTP_CLIENT_TIMEOUT": o.httpClientTimeout,
"HTTP_CLIENT_USER_AGENT": o.httpClientUserAgent,
"HTTP_SERVER_TIMEOUT": o.httpServerTimeout,
@@ -729,7 +777,7 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option {
"MEDIA_PROXY_HTTP_CLIENT_TIMEOUT": o.mediaProxyHTTPClientTimeout,
"MEDIA_PROXY_RESOURCE_TYPES": o.mediaProxyResourceTypes,
"MEDIA_PROXY_MODE": o.mediaProxyMode,
"MEDIA_PROXY_PRIVATE_KEY": redactSecretValue(string(o.mediaProxyPrivateKey), redactSecret),
"MEDIA_PROXY_PRIVATE_KEY": mediaProxyPrivateKeyValue,
"MEDIA_PROXY_CUSTOM_URL": o.mediaProxyCustomURL,
"ROOT_URL": o.rootURL,
"RUN_MIGRATIONS": o.runMigrations,
@@ -737,6 +785,7 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option {
"SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL": o.schedulerEntryFrequencyMinInterval,
"SCHEDULER_ENTRY_FREQUENCY_FACTOR": o.schedulerEntryFrequencyFactor,
"SCHEDULER_ROUND_ROBIN_MIN_INTERVAL": o.schedulerRoundRobinMinInterval,
"SCHEDULER_ROUND_ROBIN_MAX_INTERVAL": o.schedulerRoundRobinMaxInterval,
"SCHEDULER_SERVICE": o.schedulerService,
"SERVER_TIMING_HEADER": o.serverTimingHeader,
"WATCHDOG": o.watchdog,
+8 -1
View File
@@ -160,6 +160,8 @@ func (p *Parser) parseLines(lines []string) (err error) {
p.opts.schedulerEntryFrequencyFactor = parseInt(value, defaultSchedulerEntryFrequencyFactor)
case "SCHEDULER_ROUND_ROBIN_MIN_INTERVAL":
p.opts.schedulerRoundRobinMinInterval = parseInt(value, defaultSchedulerRoundRobinMinInterval)
case "SCHEDULER_ROUND_ROBIN_MAX_INTERVAL":
p.opts.schedulerRoundRobinMaxInterval = parseInt(value, defaultSchedulerRoundRobinMaxInterval)
case "POLLING_PARSING_ERROR_LIMIT":
p.opts.pollingParsingErrorLimit = parseInt(value, defaultPollingParsingErrorLimit)
case "PROXY_IMAGES":
@@ -240,7 +242,12 @@ func (p *Parser) parseLines(lines []string) (err error) {
case "HTTP_CLIENT_MAX_BODY_SIZE":
p.opts.httpClientMaxBodySize = int64(parseInt(value, defaultHTTPClientMaxBodySize) * 1024 * 1024)
case "HTTP_CLIENT_PROXY":
p.opts.httpClientProxy = parseString(value, defaultHTTPClientProxy)
p.opts.httpClientProxyURL, err = url.Parse(parseString(value, defaultHTTPClientProxy))
if err != nil {
return fmt.Errorf("config: invalid HTTP_CLIENT_PROXY value: %w", err)
}
case "HTTP_CLIENT_PROXIES":
p.opts.httpClientProxies = parseStringList(value, []string{})
case "HTTP_CLIENT_USER_AGENT":
p.opts.httpClientUserAgent = parseString(value, defaultHTTPClientUserAgent)
case "HTTP_SERVER_TIMEOUT":
+4
View File
@@ -1062,4 +1062,8 @@ var migrations = []func(tx *sql.Tx, driver string) error{
return nil
},
func(tx *sql.Tx, _ string) (err error) {
_, err = tx.Exec(`ALTER TABLE feeds ADD COLUMN proxy_url text default ''`)
return err
},
}
+20 -17
View File
@@ -20,6 +20,7 @@ import (
"miniflux.app/v2/internal/integration"
"miniflux.app/v2/internal/mediaproxy"
"miniflux.app/v2/internal/model"
"miniflux.app/v2/internal/proxyrotator"
"miniflux.app/v2/internal/reader/fetcher"
mff "miniflux.app/v2/internal/reader/handler"
mfs "miniflux.app/v2/internal/reader/subscription"
@@ -683,7 +684,7 @@ func (h *handler) quickAddHandler(w http.ResponseWriter, r *http.Request) {
requestBuilder := fetcher.NewRequestBuilder()
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
requestBuilder.WithProxyRotator(proxyrotator.ProxyRotatorInstance)
var rssBridgeURL string
if intg, err := h.store.Integration(userID); err == nil && intg != nil && intg.RSSBridgeEnabled {
@@ -735,17 +736,16 @@ func getFeed(stream Stream, store *storage.Storage, userID int64) (*model.Feed,
return store.FeedByID(userID, feedID)
}
func getOrCreateCategory(category Stream, store *storage.Storage, userID int64) (*model.Category, error) {
func getOrCreateCategory(streamCategory Stream, store *storage.Storage, userID int64) (*model.Category, error) {
switch {
case category.ID == "":
case streamCategory.ID == "":
return store.FirstCategory(userID)
case store.CategoryTitleExists(userID, category.ID):
return store.CategoryByTitle(userID, category.ID)
case store.CategoryTitleExists(userID, streamCategory.ID):
return store.CategoryByTitle(userID, streamCategory.ID)
default:
catRequest := model.CategoryRequest{
Title: category.ID,
}
return store.CreateCategory(userID, &catRequest)
return store.CreateCategory(userID, &model.CategoryCreationRequest{
Title: streamCategory.ID,
})
}
}
@@ -1143,20 +1143,23 @@ func (h *handler) renameTagHandler(w http.ResponseWriter, r *http.Request) {
json.NotFound(w, r)
return
}
categoryRequest := model.CategoryRequest{
Title: destination.ID,
categoryModificationRequest := model.CategoryModificationRequest{
Title: model.SetOptionalField(destination.ID),
}
verr := validator.ValidateCategoryModification(h.store, userID, category.ID, &categoryRequest)
if verr != nil {
json.BadRequest(w, r, verr.Error())
if validationError := validator.ValidateCategoryModification(h.store, userID, category.ID, &categoryModificationRequest); validationError != nil {
json.BadRequest(w, r, validationError.Error())
return
}
categoryRequest.Patch(category)
err = h.store.UpdateCategory(category)
if err != nil {
categoryModificationRequest.Patch(category)
if err := h.store.UpdateCategory(category); err != nil {
json.ServerError(w, r, err)
return
}
OK(w, r)
}
@@ -5,8 +5,12 @@ package telegrambot // import "miniflux.app/v2/internal/integration/telegrambot"
import (
"fmt"
"log/slog"
"strconv"
"miniflux.app/v2/internal/config"
"miniflux.app/v2/internal/model"
"miniflux.app/v2/internal/urllib"
)
func PushEntry(feed *model.Feed, entry *model.Entry, botToken, chatID string, topicID *int64, disableWebPagePreview, disableNotification bool, disableButtons bool) error {
@@ -32,8 +36,16 @@ func PushEntry(feed *model.Feed, entry *model.Entry, botToken, chatID string, to
if !disableButtons {
var markupRow []*InlineKeyboardButton
websiteURLButton := InlineKeyboardButton{Text: "Go to website", URL: feed.SiteURL}
markupRow = append(markupRow, &websiteURLButton)
baseURL := config.Opts.BaseURL()
entryPath := "/unread/entry/" + strconv.FormatInt(entry.ID, 10)
minifluxEntryURL, err := urllib.JoinBaseURLAndPath(baseURL, entryPath)
if err != nil {
slog.Error("Unable to create Miniflux entry URL", slog.Any("error", err))
} else {
minifluxEntryURLButton := InlineKeyboardButton{Text: "Go to Miniflux", URL: minifluxEntryURL}
markupRow = append(markupRow, &minifluxEntryURLButton)
}
articleURLButton := InlineKeyboardButton{Text: "Go to article", URL: entry.URL}
markupRow = append(markupRow, &articleURLButton)
+26 -23
View File
@@ -72,10 +72,10 @@
"entry.state.loading": "Lade...",
"entry.state.saving": "Speichern...",
"entry.status.mark_as_read": "Als gelesen markieren",
"entry.status.mark_as_unread": "Als ungelesen markieren",
"entry.status.title": "Status des Artikels ändern",
"entry.status.toast.read": "Als gelesen markiert",
"entry.status.toast.unread": "Als ungelesen markiert",
"entry.status.mark_as_unread": "Als ungelesen markieren",
"entry.tags.label": "Stichworte:",
"entry.unshare.label": "Nicht teilen",
"error.api_key_already_exists": "Dieser API-Schlüssel ist bereits vorhanden.",
@@ -117,6 +117,7 @@
"error.invalid_default_home_page": "Ungültige Standard-Startseite!",
"error.invalid_display_mode": "Progressive-Web-App- (PWA-)Anzeigemodus",
"error.invalid_entry_direction": "Ungültige Sortierreihenfolge.",
"error.invalid_feed_proxy_url": "Ungültige Proxy-URL.",
"error.invalid_feed_url": "Ungültiger Feed-URL.",
"error.invalid_gesture_nav": "Ungültige Gestennavigation.",
"error.invalid_language": "Ungültige Sprache.",
@@ -128,6 +129,7 @@
"error.password_min_length": "Wenigstens 6 Zeichen müssen genutzt werden.",
"error.pocket_access_token": "Zugriffstoken konnte nicht von Pocket abgerufen werden!",
"error.pocket_request_token": "Anfrage-Token konnte nicht von Pocket abgerufen werden!",
"error.proxy_url_not_empty": "Die Proxy-URL darf nicht leer sein.",
"error.settings_block_rule_fieldname_invalid": "Ungültige Blockierregel: Regel #%d hat keinen gültigen Feldnamen (Optionen: %s)",
"error.settings_block_rule_invalid_regex": "Ungültige Blockierregel: Das Muster für Regel #%d ist kein zulässiger regulärer Ausdruck",
"error.settings_block_rule_regex_required": "Ungültige Blockierregel: Regel #%d hat kein Muster",
@@ -174,7 +176,7 @@
"form.feed.label.feed_password": "Passwort des Abonnements",
"form.feed.label.feed_url": "URL des Abonnements",
"form.feed.label.feed_username": "Benutzername des Abonnements",
"form.feed.label.fetch_via_proxy": "Über Proxy abrufen",
"form.feed.label.fetch_via_proxy": "Den auf Anwendungsebene konfigurierten Proxy verwenden",
"form.feed.label.hide_globally": "Einträge in der globalen Ungelesen-Liste ausblenden",
"form.feed.label.ignore_http_cache": "Ignoriere HTTP-Cache",
"form.feed.label.keeplist_rules": "Erlaubnisregeln",
@@ -187,13 +189,14 @@
"form.feed.label.ntfy_min_priority": "Niedrigste Ntfy-Priorität",
"form.feed.label.ntfy_priority": "Ntfy-Priorität",
"form.feed.label.ntfy_topic": "Ntfy-Thema (optional)",
"form.feed.label.pushover_activate": "Push entries to pushover.net",
"form.feed.label.pushover_default_priority": "Pushover default priority",
"form.feed.label.pushover_high_priority": "Pushover high priority",
"form.feed.label.pushover_low_priority": "Pushover low priority",
"form.feed.label.pushover_max_priority": "Pushover max priority",
"form.feed.label.pushover_min_priority": "Pushover min priority",
"form.feed.label.pushover_priority": "Pushover message priority",
"form.feed.label.proxy_url": "Proxy-URL",
"form.feed.label.pushover_activate": "Einträge an pushover.net senden",
"form.feed.label.pushover_default_priority": "Pushover-Standardpriorität",
"form.feed.label.pushover_high_priority": "Hohe Pushoverpriorität",
"form.feed.label.pushover_low_priority": "Niedrige Pushoverpriorität",
"form.feed.label.pushover_max_priority": "Höchste Pushoverpriorität",
"form.feed.label.pushover_min_priority": "Niedrigste Pushoverpriorität",
"form.feed.label.pushover_priority": "Pushover-Nachrichtenpriorität",
"form.feed.label.rewrite_rules": "Umschreiberegeln",
"form.feed.label.scraper_rules": "Extraktionsregeln",
"form.feed.label.site_url": "URL der Webseite",
@@ -272,11 +275,11 @@
"form.integration.pocket_activate": "Einträge in Pocket speichern",
"form.integration.pocket_connect_link": "Verbinden Sie Ihr Pocket-Konto",
"form.integration.pocket_consumer_key": "Pocket-Verbraucher-Schlüssel",
"form.integration.pushover_activate": "Push entries to Pushover",
"form.integration.pushover_device": "Pushover device (optional)",
"form.integration.pushover_prefix": "Pushover URL prefix (optional)",
"form.integration.pushover_token": "Pushover application API token",
"form.integration.pushover_user": "Pushover user key",
"form.integration.pushover_activate": "Einträge an Pushover senden",
"form.integration.pushover_device": "Pushovergerät (optional)",
"form.integration.pushover_prefix": "Pushover-URL-Präfix (optional)",
"form.integration.pushover_token": "Pushover-Anwendungs-API-Token",
"form.integration.pushover_user": "Pushover-Benutzerschlüssel",
"form.integration.raindrop_activate": "Einträge in Raindrop speichern",
"form.integration.raindrop_collection_id": "Sammlungs-ID",
"form.integration.raindrop_tags": "Tags (kommagetrennt)",
@@ -401,7 +404,7 @@
"page.about.author": "Autor:",
"page.about.build_date": "Datum der Kompilierung:",
"page.about.credits": "Urheberrechte",
"page.about.db_usage": "Database size:",
"page.about.db_usage": "Datenbankgröße:",
"page.about.global_config_options": "Globale Konfigurationsoptionen",
"page.about.go_version": "Go-Version:",
"page.about.license": "Lizenz:",
@@ -421,6 +424,10 @@
"page.api_keys.table.last_used_at": "Zuletzt verwendeten",
"page.api_keys.table.token": "Zeichen",
"page.api_keys.title": "API-Schlüssel",
"page.categories_count": [
"%d Kategorie",
"%d Kategorien"
],
"page.categories.entries": "Artikel",
"page.categories.feed_count": [
"Es gibt %d Abonnement.",
@@ -429,10 +436,6 @@
"page.categories.feeds": "Abonnements",
"page.categories.no_feed": "Kein Abonnement.",
"page.categories.title": "Kategorien",
"page.categories_count": [
"%d Kategorie",
"%d Kategorien"
],
"page.category_label": "Kategorie: %s",
"page.edit_category.title": "Kategorie bearbeiten: %s",
"page.edit_feed.etag_header": "ETag-Kopfzeile:",
@@ -538,25 +541,25 @@
"page.settings.webauthn.passkeys": "Passkeys",
"page.settings.webauthn.register": "Hauptschlüssel registrieren",
"page.settings.webauthn.register.error": "Hauptschlüssel kann nicht registriert werden",
"page.shared_entries.title": "Geteilte Artikel",
"page.shared_entries_count": [
"%d geteilter Artikel",
"%d geteilte Artikel"
],
"page.starred.title": "Lesezeichen",
"page.shared_entries.title": "Geteilte Artikel",
"page.starred_entry_count": [
"%d Lesezeichen",
"%d Lesezeichen"
],
"page.starred.title": "Lesezeichen",
"page.total_entry_count": [
"%d Artikel insgesamt",
"%d Artikel insgesamt"
],
"page.unread.title": "Ungelesen",
"page.unread_entry_count": [
"%d ungelesener Artikel",
"%d ungelesene Artikel"
],
"page.unread.title": "Ungelesen",
"page.users.actions": "Aktionen",
"page.users.admin.no": "Nein",
"page.users.admin.yes": "Ja",
@@ -603,4 +606,4 @@
"time_elapsed.yesterday": "gestern",
"tooltip.keyboard_shortcuts": "Tastenkürzel: %s",
"tooltip.logged_user": "Angemeldet als %s"
}
}
+13 -10
View File
@@ -72,10 +72,10 @@
"entry.state.loading": "Φόρτωση...",
"entry.state.saving": "Aποθήκευση...",
"entry.status.mark_as_read": "Επισήμανση ως αναγνωσμένο",
"entry.status.mark_as_unread": "Επισήμανση ως μη αναγνωσμένο",
"entry.status.title": "Αλλαγή κατάστασης καταχώρησης",
"entry.status.toast.read": "Επισήμανση ως αναγνωσμένο",
"entry.status.toast.unread": "Επισήμανση ως μη αναγνωσμένο",
"entry.status.mark_as_unread": "Επισήμανση ως μη αναγνωσμένο",
"entry.tags.label": "Ετικέτες:",
"entry.unshare.label": "Aναίρεση Διαμοιρασμού",
"error.api_key_already_exists": "Αυτό το κλειδί API υπάρχει ήδη.",
@@ -117,6 +117,7 @@
"error.invalid_default_home_page": "Μη έγκυρη προεπιλεγμένη αρχική σελίδα!",
"error.invalid_display_mode": "Μη έγκυρη λειτουργία εμφάνισης εφαρμογών ιστού.",
"error.invalid_entry_direction": "Μη έγκυρη κατεύθυνση ταξινόμησης άρθρων.",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "Μη έγκυρη διεύθυνση URL ροής.",
"error.invalid_gesture_nav": "Μη έγκυρη πλοήγηση με χειρονομίες.",
"error.invalid_language": "Μη έγκυρη γλώσσα.",
@@ -128,6 +129,7 @@
"error.password_min_length": "Ο κωδικός πρόσβασης πρέπει να έχει τουλάχιστον 6 χαρακτήρες.",
"error.pocket_access_token": "Δεν είναι δυνατή η λήψη του access token από το Pocket!",
"error.pocket_request_token": "Δεν είναι δυνατή η λήψη του request token από το Pocket!",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
@@ -174,7 +176,7 @@
"form.feed.label.feed_password": "Κωδικός Πρόσβασης ροής",
"form.feed.label.feed_url": "Διεύθυνση URL ροής",
"form.feed.label.feed_username": "Όνομα Χρήστη ροής",
"form.feed.label.fetch_via_proxy": "Λήψη μέσω διακομιστή μεσολάβησης",
"form.feed.label.fetch_via_proxy": "Χρησιμοποιήστε τον διακομιστή μεσολάβησης που έχει ρυθμιστεί σε επίπεδο εφαρμογής",
"form.feed.label.hide_globally": "Απόκρυψη καταχωρήσεων σε γενική λίστα μη αναγνωσμένων",
"form.feed.label.ignore_http_cache": "Αγνοήστε την προσωρινή μνήμη HTTP",
"form.feed.label.keeplist_rules": "Κρατήστε Κανόνες",
@@ -187,6 +189,7 @@
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
"form.feed.label.ntfy_priority": "Ntfy priority",
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "Push entries to pushover.net",
"form.feed.label.pushover_default_priority": "Pushover default priority",
"form.feed.label.pushover_high_priority": "Pushover high priority",
@@ -421,6 +424,10 @@
"page.api_keys.table.last_used_at": "Τελευταία Χρήση",
"page.api_keys.table.token": "Token",
"page.api_keys.title": "Κλειδιά API",
"page.categories_count": [
"%d category",
"%d categories"
],
"page.categories.entries": "Άρθρα",
"page.categories.feed_count": [
"Υπάρχει μία %d ροή.",
@@ -429,10 +436,6 @@
"page.categories.feeds": "Συνδρομές",
"page.categories.no_feed": "Καμία ροή.",
"page.categories.title": "Κατηγορίες",
"page.categories_count": [
"%d category",
"%d categories"
],
"page.category_label": "Category: %s",
"page.edit_category.title": "Επεξεργασία κατηγορίας: % s",
"page.edit_feed.etag_header": "Κεφαλίδα ETag:",
@@ -538,25 +541,25 @@
"page.settings.webauthn.passkeys": "Passkeys",
"page.settings.webauthn.register": "Εγγραφή κωδικού πρόσβασης",
"page.settings.webauthn.register.error": "Δεν είναι δυνατή η εγγραφή του κωδικού πρόσβασης",
"page.shared_entries.title": "Κοινόχρηστες Καταχωρήσεις",
"page.shared_entries_count": [
"%d shared entry",
"%d shared entries"
],
"page.starred.title": "Αγαπημένo",
"page.shared_entries.title": "Κοινόχρηστες Καταχωρήσεις",
"page.starred_entry_count": [
"%d starred entry",
"%d starred entries"
],
"page.starred.title": "Αγαπημένo",
"page.total_entry_count": [
"%d entry in total",
"%d entries in total"
],
"page.unread.title": "Μη αναγνωσμένα",
"page.unread_entry_count": [
"%d unread entry",
"%d unread entries"
],
"page.unread.title": "Μη αναγνωσμένα",
"page.users.actions": "Eνέργειες",
"page.users.admin.no": "Όχι",
"page.users.admin.yes": "Ναι.",
@@ -603,4 +606,4 @@
"time_elapsed.yesterday": "χθες",
"tooltip.keyboard_shortcuts": "Συντόμευση πληκτρολογίου: % s",
"tooltip.logged_user": "Συνδεδεμένος/η ως %s"
}
}
+5 -2
View File
@@ -72,10 +72,10 @@
"entry.state.loading": "Loading…",
"entry.state.saving": "Saving…",
"entry.status.mark_as_read": "Mark as read",
"entry.status.mark_as_unread": "Mark as unread",
"entry.status.title": "Change entry status",
"entry.status.toast.read": "Marked as read",
"entry.status.toast.unread": "Marked as unread",
"entry.status.mark_as_unread": "Mark as unread",
"entry.tags.label": "Tags:",
"entry.unshare.label": "Unshare",
"error.api_key_already_exists": "This API Key already exists.",
@@ -117,6 +117,7 @@
"error.invalid_default_home_page": "Invalid default homepage!",
"error.invalid_display_mode": "Invalid web app display mode.",
"error.invalid_entry_direction": "Invalid entry direction.",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "Invalid feed URL.",
"error.invalid_gesture_nav": "Invalid gesture navigation.",
"error.invalid_language": "Invalid language.",
@@ -128,6 +129,7 @@
"error.password_min_length": "The password must have at least 6 characters.",
"error.pocket_access_token": "Unable to fetch access token from Pocket!",
"error.pocket_request_token": "Unable to fetch request token from Pocket!",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
@@ -174,7 +176,7 @@
"form.feed.label.feed_password": "Feed Password",
"form.feed.label.feed_url": "Feed URL",
"form.feed.label.feed_username": "Feed Username",
"form.feed.label.fetch_via_proxy": "Fetch via proxy",
"form.feed.label.fetch_via_proxy": "Use the proxy configured at the application level",
"form.feed.label.hide_globally": "Hide entries in global unread list",
"form.feed.label.ignore_http_cache": "Ignore HTTP cache",
"form.feed.label.keeplist_rules": "Keep Rules",
@@ -187,6 +189,7 @@
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
"form.feed.label.ntfy_priority": "Ntfy priority",
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "Push entries to Pushover",
"form.feed.label.pushover_default_priority": "Default priority",
"form.feed.label.pushover_high_priority": "High priority",
+13 -10
View File
@@ -72,10 +72,10 @@
"entry.state.loading": "Cargando...",
"entry.state.saving": "Guardando...",
"entry.status.mark_as_read": "Marcar como leído",
"entry.status.mark_as_unread": "Marcar como no leído",
"entry.status.title": "Cambiar estado del artículo",
"entry.status.toast.read": "Marcado como leído",
"entry.status.toast.unread": "Marcado como no leído",
"entry.status.mark_as_unread": "Marcar como no leído",
"entry.tags.label": "Etiquetas:",
"entry.unshare.label": "No compartir",
"error.api_key_already_exists": "Esta clave API ya existe.",
@@ -117,6 +117,7 @@
"error.invalid_default_home_page": "¡Página de inicio por defecto no válida!",
"error.invalid_display_mode": "Modo de visualización de la aplicación web no válido.",
"error.invalid_entry_direction": "Dirección de artículo no válida.",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "URL de feed no válida.",
"error.invalid_gesture_nav": "Navegación por gestos no válida.",
"error.invalid_language": "Idioma no válido.",
@@ -128,6 +129,7 @@
"error.password_min_length": "La contraseña debería tener al menos 6 caracteres.",
"error.pocket_access_token": "Incapaz de obtener un token de acceso de Pocket!",
"error.pocket_request_token": "Incapaz de obtener un token de solicitud de Pocket!",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"error.settings_block_rule_fieldname_invalid": "Regla de bloqueo no válida: a la regla #%d le falta un nombre de campo válido (Opciones: %s)",
"error.settings_block_rule_invalid_regex": "Regla de bloqueo no válida: el patrón de la regla #%d no es una expresión regular válida",
"error.settings_block_rule_regex_required": "Regla de bloqueo no válida: no se ha proporcionado el patrón de la regla #%d",
@@ -174,7 +176,7 @@
"form.feed.label.feed_password": "Contraseña de la fuente",
"form.feed.label.feed_url": "URL de la fuente",
"form.feed.label.feed_username": "Nombre de usuario de la fuente",
"form.feed.label.fetch_via_proxy": "Buscar a través de proxy",
"form.feed.label.fetch_via_proxy": "Usar el proxy configurado a nivel de la aplicación",
"form.feed.label.hide_globally": "Ocultar artículos en la lista global de no leídos",
"form.feed.label.ignore_http_cache": "Ignorar caché HTTP",
"form.feed.label.keeplist_rules": "Reglas de Filtrado (Permitir)",
@@ -187,6 +189,7 @@
"form.feed.label.ntfy_min_priority": "Prioridad mínima a Ntfy",
"form.feed.label.ntfy_priority": "Prioridad Ntfy",
"form.feed.label.ntfy_topic": "Tema Ntfy (opcional)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "Push entries to pushover.net",
"form.feed.label.pushover_default_priority": "Pushover default priority",
"form.feed.label.pushover_high_priority": "Pushover high priority",
@@ -421,6 +424,10 @@
"page.api_keys.table.last_used_at": "Último utilizado",
"page.api_keys.table.token": "simbólico",
"page.api_keys.title": "Claves API",
"page.categories_count": [
"%d categoría",
"%d categorías"
],
"page.categories.entries": "Artículos",
"page.categories.feed_count": [
"Hay %d fuente.",
@@ -429,10 +436,6 @@
"page.categories.feeds": "Fuentes",
"page.categories.no_feed": "Sin fuente.",
"page.categories.title": "Categorías",
"page.categories_count": [
"%d categoría",
"%d categorías"
],
"page.category_label": "Categoría: %s",
"page.edit_category.title": "Editar categoría: %s",
"page.edit_feed.etag_header": "Cabecera de ETag:",
@@ -538,25 +541,25 @@
"page.settings.webauthn.passkeys": "Claves de acceso",
"page.settings.webauthn.register": "Registrar clave de acceso",
"page.settings.webauthn.register.error": "No se puede registrar la clave de acceso",
"page.shared_entries.title": "Artículos compartidos",
"page.shared_entries_count": [
"%d artículo compartido",
"%d artículos compartidos"
],
"page.starred.title": "Marcadores",
"page.shared_entries.title": "Artículos compartidos",
"page.starred_entry_count": [
"%d artículo marcado",
"%d artículos marcados"
],
"page.starred.title": "Marcadores",
"page.total_entry_count": [
"%d artículo en total",
"%d artículos en total"
],
"page.unread.title": "No leídos",
"page.unread_entry_count": [
"%d artículo no leído",
"%d artículos no leídos"
],
"page.unread.title": "No leídos",
"page.users.actions": "Acciones",
"page.users.admin.no": "No",
"page.users.admin.yes": "Sí",
@@ -603,4 +606,4 @@
"time_elapsed.yesterday": "ayer",
"tooltip.keyboard_shortcuts": "Atajo de teclado: %s",
"tooltip.logged_user": "Registrado como %s"
}
}
+13 -10
View File
@@ -72,10 +72,10 @@
"entry.state.loading": "Ladataan...",
"entry.state.saving": "Tallennetaan...",
"entry.status.mark_as_read": "Merkitse luetuksi",
"entry.status.mark_as_unread": "Merkitse lukemattomaksi",
"entry.status.title": "Vaihda artikkelin tilaa",
"entry.status.toast.read": "Merkitty luetuksi",
"entry.status.toast.unread": "Merkitty lukemattomaksi",
"entry.status.mark_as_unread": "Merkitse lukemattomaksi",
"entry.tags.label": "Tags:",
"entry.unshare.label": "Poista jako",
"error.api_key_already_exists": "API-avain on jo olemassa.",
@@ -117,6 +117,7 @@
"error.invalid_default_home_page": "Väärä oletusarvoinen kotisivu!",
"error.invalid_display_mode": "Virheellinen verkkosovelluksen näyttötila.",
"error.invalid_entry_direction": "Invalid entry direction.",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "Virheellinen syötteen URL-osoite.",
"error.invalid_gesture_nav": "Virheellinen ele-navigointi.",
"error.invalid_language": "Virheellinen kieli.",
@@ -128,6 +129,7 @@
"error.password_min_length": "Salasanassa on oltava vähintään 6 merkkiä.",
"error.pocket_access_token": "Unable to fetch access token from Pocket!",
"error.pocket_request_token": "Unable to fetch request token from Pocket!",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
@@ -174,7 +176,7 @@
"form.feed.label.feed_password": "Syötteen salasana",
"form.feed.label.feed_url": "Syötteen URL-osoite",
"form.feed.label.feed_username": "Syötteen käyttäjätunnus",
"form.feed.label.fetch_via_proxy": "Nouda välityspalvelimen kautta",
"form.feed.label.fetch_via_proxy": "Käytä sovellustasolla määritettyä välityspalvelinta",
"form.feed.label.hide_globally": "Piilota artikkelit lukemattomien listassa",
"form.feed.label.ignore_http_cache": "Ohita HTTP-välimuisti",
"form.feed.label.keeplist_rules": "Keep-säännöt",
@@ -187,6 +189,7 @@
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
"form.feed.label.ntfy_priority": "Ntfy priority",
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "Push entries to pushover.net",
"form.feed.label.pushover_default_priority": "Pushover default priority",
"form.feed.label.pushover_high_priority": "Pushover high priority",
@@ -421,6 +424,10 @@
"page.api_keys.table.last_used_at": "Viimeksi käytetty",
"page.api_keys.table.token": "Tunnus",
"page.api_keys.title": "API-avaimet",
"page.categories_count": [
"%d category",
"%d categories"
],
"page.categories.entries": "Artikkelit",
"page.categories.feed_count": [
"On %d syöte.",
@@ -429,10 +436,6 @@
"page.categories.feeds": "Tilaukset",
"page.categories.no_feed": "Ei syötettä.",
"page.categories.title": "Kategoriat",
"page.categories_count": [
"%d category",
"%d categories"
],
"page.category_label": "Category: %s",
"page.edit_category.title": "Muokkaa kategoria: %s",
"page.edit_feed.etag_header": "ETag-otsikko:",
@@ -538,25 +541,25 @@
"page.settings.webauthn.passkeys": "Passkeys",
"page.settings.webauthn.register": "Rekisteröi salasana",
"page.settings.webauthn.register.error": "Salasanaa ei voi rekisteröidä",
"page.shared_entries.title": "Jaetut artikkelit",
"page.shared_entries_count": [
"%d shared entry",
"%d shared entries"
],
"page.starred.title": "Suosikit",
"page.shared_entries.title": "Jaetut artikkelit",
"page.starred_entry_count": [
"%d starred entry",
"%d starred entries"
],
"page.starred.title": "Suosikit",
"page.total_entry_count": [
"%d entry in total",
"%d entries in total"
],
"page.unread.title": "Lukemattomat",
"page.unread_entry_count": [
"%d unread entry",
"%d unread entries"
],
"page.unread.title": "Lukemattomat",
"page.users.actions": "Toiminnot",
"page.users.admin.no": "Ei",
"page.users.admin.yes": "Kyllä",
@@ -603,4 +606,4 @@
"time_elapsed.yesterday": "eilen",
"tooltip.keyboard_shortcuts": "Pikanäppäin: %s",
"tooltip.logged_user": "Kirjautunut %s-käyttäjänä"
}
}
+13 -10
View File
@@ -72,10 +72,10 @@
"entry.state.loading": "Chargement...",
"entry.state.saving": "Sauvegarde en cours...",
"entry.status.mark_as_read": "Marquer comme lu",
"entry.status.mark_as_unread": "Marquer comme non lu",
"entry.status.title": "Changer le statut de l'entrée",
"entry.status.toast.read": "Marqué comme lu",
"entry.status.toast.unread": "Marqué comme non lu",
"entry.status.mark_as_unread": "Marquer comme non lu",
"entry.tags.label": "Libellés :",
"entry.unshare.label": "Enlever le partage",
"error.api_key_already_exists": "Cette clé d'API existe déjà.",
@@ -117,6 +117,7 @@
"error.invalid_default_home_page": "Page d'accueil par défaut invalide !",
"error.invalid_display_mode": "Mode d'affichage de l'application web non valide.",
"error.invalid_entry_direction": "Ordre de trie non valide.",
"error.invalid_feed_proxy_url": "L'URL du proxy n'est pas valide.",
"error.invalid_feed_url": "URL de flux non valide.",
"error.invalid_gesture_nav": "Navigation gestuelle non valide.",
"error.invalid_language": "Langue non valide.",
@@ -128,6 +129,7 @@
"error.password_min_length": "Vous devez utiliser au moins 6 caractères pour le mot de passe.",
"error.pocket_access_token": "Impossible de récupérer le jeton d'accès depuis Pocket !",
"error.pocket_request_token": "Impossible de récupérer le jeton d'accès depuis Pocket !",
"error.proxy_url_not_empty": "L'URL du proxy ne peut pas être vide.",
"error.settings_block_rule_fieldname_invalid": "Règle de blocage invalide : la règle n°%d ne contient pas un nom de champ valide (Options : %s)",
"error.settings_block_rule_invalid_regex": "Règle de blocage invalide : le motif de la règle n°%d n'est pas une expression régulière valide",
"error.settings_block_rule_regex_required": "Règle de blocage invalide : le motif de la règle n°%d n'est pas fourni",
@@ -174,7 +176,7 @@
"form.feed.label.feed_password": "Mot de passe du flux",
"form.feed.label.feed_url": "URL du flux",
"form.feed.label.feed_username": "Nom d'utilisateur du flux",
"form.feed.label.fetch_via_proxy": "Récupérer via proxy",
"form.feed.label.fetch_via_proxy": "Utiliser le proxy configuré au niveau de l'application",
"form.feed.label.hide_globally": "Masquer les entrées dans la liste globale non lue",
"form.feed.label.ignore_http_cache": "Ignorer le cache HTTP",
"form.feed.label.keeplist_rules": "Règles d'autorisation",
@@ -187,6 +189,7 @@
"form.feed.label.ntfy_min_priority": "Priorité minimale de notification",
"form.feed.label.ntfy_priority": "Priorité de notification",
"form.feed.label.ntfy_topic": "Sujet Ntfy (facultatif)",
"form.feed.label.proxy_url": "URL du proxy",
"form.feed.label.pushover_activate": "Activer les notifications vers Pushover",
"form.feed.label.pushover_default_priority": "Priorité par défaut",
"form.feed.label.pushover_high_priority": "Priorité élevée",
@@ -421,6 +424,10 @@
"page.api_keys.table.last_used_at": "Dernière utilisation",
"page.api_keys.table.token": "Jeton",
"page.api_keys.title": "Clés d'API",
"page.categories_count": [
"%d catégorie",
"%d catégories"
],
"page.categories.entries": "Articles",
"page.categories.feed_count": [
"Il y a %d abonnement.",
@@ -429,10 +436,6 @@
"page.categories.feeds": "Abonnements",
"page.categories.no_feed": "Aucun abonnement.",
"page.categories.title": "Catégories",
"page.categories_count": [
"%d catégorie",
"%d catégories"
],
"page.category_label": "Catégorie : %s",
"page.edit_category.title": "Modification de la catégorie : %s",
"page.edit_feed.etag_header": "En-tête ETag :",
@@ -538,25 +541,25 @@
"page.settings.webauthn.passkeys": "Clés daccès",
"page.settings.webauthn.register": "Enregister une nouvelle clé daccès",
"page.settings.webauthn.register.error": "Impossible d'enregistrer la clé daccès",
"page.shared_entries.title": "Articles partagés",
"page.shared_entries_count": [
"%d article partagé",
"%d articles partagés"
],
"page.starred.title": "Favoris",
"page.shared_entries.title": "Articles partagés",
"page.starred_entry_count": [
"%d favori",
"%d favoris"
],
"page.starred.title": "Favoris",
"page.total_entry_count": [
"%d article au total",
"%d articles au total"
],
"page.unread.title": "Non lus",
"page.unread_entry_count": [
"%d article non lu",
"%d articles non lus"
],
"page.unread.title": "Non lus",
"page.users.actions": "Actions",
"page.users.admin.no": "Non",
"page.users.admin.yes": "Oui",
@@ -603,4 +606,4 @@
"time_elapsed.yesterday": "hier",
"tooltip.keyboard_shortcuts": "Raccourci clavier : %s",
"tooltip.logged_user": "Connecté en tant que %s"
}
}
+13 -10
View File
@@ -72,10 +72,10 @@
"entry.state.loading": "लोड हो रहा है...",
"entry.state.saving": "सहेजा जा रहा है...",
"entry.status.mark_as_read": "पढ़े हुए का चिह्न",
"entry.status.mark_as_unread": "अपठित के रूप में चिह्नित करें",
"entry.status.title": "प्रविष्टि स्थिति बदलें",
"entry.status.toast.read": "पढ़ा हुआ चिह्नित करे",
"entry.status.toast.unread": "अपठित के रूप में चिह्नित",
"entry.status.mark_as_unread": "अपठित के रूप में चिह्नित करें",
"entry.tags.label": "टैग:",
"entry.unshare.label": "न साझा कारें",
"error.api_key_already_exists": "यह एपीआई कुंजी पहले से मौजूद है।",
@@ -117,6 +117,7 @@
"error.invalid_default_home_page": "अमान्य डिफ़ॉल्ट मुखपृष्ठ!",
"error.invalid_display_mode": "अमान्य वेब ऐप्लिकेशन प्रदर्शन मोड.",
"error.invalid_entry_direction": "अमान्य प्रवेश दिशा।",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "दृष्टिकोण यूआरएल.",
"error.invalid_gesture_nav": "अमान्य इशारा नेविगेशन।",
"error.invalid_language": "अमान्य भाषा.",
@@ -128,6 +129,7 @@
"error.password_min_length": "पासवर्ड में कम से कम 6 अक्षर होने चाहिए।",
"error.pocket_access_token": "पॉकेट से एक्सेस टोकन प्राप्त करने में असमर्थ!",
"error.pocket_request_token": "पॉकेट से अनुरोध टोकन लाने में असमर्थ!",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
@@ -174,7 +176,7 @@
"form.feed.label.feed_password": "फ़ीड पासवर्ड",
"form.feed.label.feed_url": "फ़ीड यूआरएल",
"form.feed.label.feed_username": "फ़ीड उपयोगकर्ता नाम",
"form.feed.label.fetch_via_proxy": "प्रॉक्सी के माध्यम से प्राप्त करें",
"form.feed.label.fetch_via_proxy": "प्लिकेशन स्तर पर कॉन्फ़िगर किए गए प्रॉक्सी का उपयोग करें",
"form.feed.label.hide_globally": "वैश्विक अपठित सूची में प्रविष्टियां छिपाएं",
"form.feed.label.ignore_http_cache": "एचटीटीपी कैश पर ध्यान न दें",
"form.feed.label.keeplist_rules": "नियम बनाए रखें",
@@ -187,6 +189,7 @@
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
"form.feed.label.ntfy_priority": "Ntfy priority",
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "Push entries to pushover.net",
"form.feed.label.pushover_default_priority": "Pushover default priority",
"form.feed.label.pushover_high_priority": "Pushover high priority",
@@ -421,6 +424,10 @@
"page.api_keys.table.last_used_at": "आखरी इस्त्तमाल किया गया",
"page.api_keys.table.token": "टोकन",
"page.api_keys.title": "एपीआई कुंजी",
"page.categories_count": [
"%d category",
"%d categories"
],
"page.categories.entries": "विषयवस्तुया",
"page.categories.feed_count": [
"%d फ़ीड बाकी है।",
@@ -429,10 +436,6 @@
"page.categories.feeds": "सदस्यता ले",
"page.categories.no_feed": "कोई फ़ीड नहीं है।",
"page.categories.title": "श्रेणियाँ",
"page.categories_count": [
"%d category",
"%d categories"
],
"page.category_label": "Category: %s",
"page.edit_category.title": "%s श्रेणी संपाद करे",
"page.edit_feed.etag_header": "ईटाग हैडर:",
@@ -538,25 +541,25 @@
"page.settings.webauthn.passkeys": "Passkeys",
"page.settings.webauthn.register": "रजिस्टर पासकी",
"page.settings.webauthn.register.error": "पासकी पंजीकृत करने में असमर्थ",
"page.shared_entries.title": "साझा किया हुआ प्रविष्टि",
"page.shared_entries_count": [
"%d shared entry",
"%d shared entries"
],
"page.starred.title": "तारांकित",
"page.shared_entries.title": "साझा किया हुआ प्रविष्टि",
"page.starred_entry_count": [
"%d starred entry",
"%d starred entries"
],
"page.starred.title": "तारांकित",
"page.total_entry_count": [
"%d entry in total",
"%d entries in total"
],
"page.unread.title": "अपठित",
"page.unread_entry_count": [
"%d unread entry",
"%d unread entries"
],
"page.unread.title": "अपठित",
"page.users.actions": "कार्रवाई",
"page.users.admin.no": "नहीं",
"page.users.admin.yes": "हां",
@@ -603,4 +606,4 @@
"time_elapsed.yesterday": "कल",
"tooltip.keyboard_shortcuts": "कुंजीपटल संक्षिप्त रीति: %s",
"tooltip.logged_user": "%s के रूप में लॉग इन किया"
}
}
+12 -9
View File
@@ -70,10 +70,10 @@
"entry.state.loading": "Memuat...",
"entry.state.saving": "Menyimpan...",
"entry.status.mark_as_read": "Telah dibaca",
"entry.status.mark_as_unread": "Belum dibaca",
"entry.status.title": "Ubah status entri",
"entry.status.toast.read": "Ditandai sebagai telah dibaca",
"entry.status.toast.unread": "Ditandai sebagai belum dibaca",
"entry.status.mark_as_unread": "Belum dibaca",
"entry.tags.label": "Tanda:",
"entry.unshare.label": "Batal bagikan",
"error.api_key_already_exists": "Kunci API ini sudah ada.",
@@ -115,6 +115,7 @@
"error.invalid_default_home_page": "Beranda baku tidak valid!",
"error.invalid_display_mode": "Mode tampilan aplikasi web tidak valid.",
"error.invalid_entry_direction": "Urutan entri tidak valid.",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "URL umpan tidak valid.",
"error.invalid_gesture_nav": "Navigasi gestur tidak valid.",
"error.invalid_language": "Bahasa tidak valid.",
@@ -126,6 +127,7 @@
"error.password_min_length": "Kata sandi harus memiliki setidaknya 6 karakter.",
"error.pocket_access_token": "Tidak bisa mendapatkan token akses dari Pocket!",
"error.pocket_request_token": "Tidak bisa mendapatkan token permintaan dari Pocket!",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
@@ -172,7 +174,7 @@
"form.feed.label.feed_password": "Kata Sandi Umpan",
"form.feed.label.feed_url": "URL Umpan",
"form.feed.label.feed_username": "Nama Pengguna Umpan",
"form.feed.label.fetch_via_proxy": "Ambil via Proksi",
"form.feed.label.fetch_via_proxy": "Gunakan proxy yang dikonfigurasi di tingkat aplikasi",
"form.feed.label.hide_globally": "Sembunyikan entri di daftar belum dibaca global",
"form.feed.label.ignore_http_cache": "Abaikan Tembolok HTTP",
"form.feed.label.keeplist_rules": "Aturan Simpan",
@@ -185,6 +187,7 @@
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
"form.feed.label.ntfy_priority": "Ntfy priority",
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "Push entries to pushover.net",
"form.feed.label.pushover_default_priority": "Pushover default priority",
"form.feed.label.pushover_high_priority": "Pushover high priority",
@@ -419,6 +422,9 @@
"page.api_keys.table.last_used_at": "Terakhir Digunakan",
"page.api_keys.table.token": "Token",
"page.api_keys.title": "Kunci API",
"page.categories_count": [
"%d category"
],
"page.categories.entries": "Artikel",
"page.categories.feed_count": [
"Ada %d umpan."
@@ -426,9 +432,6 @@
"page.categories.feeds": "Langganan",
"page.categories.no_feed": "Tidak ada umpan.",
"page.categories.title": "Kategori",
"page.categories_count": [
"%d category"
],
"page.category_label": "Category: %s",
"page.edit_category.title": "Sunting Kategori: %s",
"page.edit_feed.etag_header": "Tajuk ETag:",
@@ -531,21 +534,21 @@
"page.settings.webauthn.passkeys": "Passkeys",
"page.settings.webauthn.register": "Register passkey",
"page.settings.webauthn.register.error": "Unable to register passkey",
"page.shared_entries.title": "Entri yang Dibagikan",
"page.shared_entries_count": [
"%d shared entry"
],
"page.starred.title": "Markah",
"page.shared_entries.title": "Entri yang Dibagikan",
"page.starred_entry_count": [
"%d starred entry"
],
"page.starred.title": "Markah",
"page.total_entry_count": [
"%d entry in total"
],
"page.unread.title": "Belum Dibaca",
"page.unread_entry_count": [
"%d unread entry"
],
"page.unread.title": "Belum Dibaca",
"page.users.actions": "Tindakan",
"page.users.admin.no": "Tidak",
"page.users.admin.yes": "Ya",
@@ -586,4 +589,4 @@
"time_elapsed.yesterday": "kemarin",
"tooltip.keyboard_shortcuts": "Pintasan Papan Tik: %s",
"tooltip.logged_user": "Masuk sebagai %s"
}
}
+13 -10
View File
@@ -72,10 +72,10 @@
"entry.state.loading": "Caricamento in corso...",
"entry.state.saving": "Salvataggio in corso...",
"entry.status.mark_as_read": "Segna come letto",
"entry.status.mark_as_unread": "Segna come non letto",
"entry.status.title": "Cambia lo stato dell'articolo",
"entry.status.toast.read": "Contrassegnato come letto",
"entry.status.toast.unread": "Contrassegnato come non letto",
"entry.status.mark_as_unread": "Segna come non letto",
"entry.tags.label": "Tag:",
"entry.unshare.label": "Unshare",
"error.api_key_already_exists": "Questa chiave API esiste già.",
@@ -117,6 +117,7 @@
"error.invalid_default_home_page": "Pagina iniziale predefinita non valida!",
"error.invalid_display_mode": "Modalità di visualizzazione web app non valida.",
"error.invalid_entry_direction": "Ordinamento non valido.",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "URL del feed non valido.",
"error.invalid_gesture_nav": "Navigazione gestuale non valida.",
"error.invalid_language": "Lingua non valida.",
@@ -128,6 +129,7 @@
"error.password_min_length": "La password deve contenere almeno 6 caratteri.",
"error.pocket_access_token": "Non sono riuscito ad ottenere l'access token da Pocket!",
"error.pocket_request_token": "Non sono riuscito ad ottenere il request token da Pocket!",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
@@ -174,7 +176,7 @@
"form.feed.label.feed_password": "Password del feed",
"form.feed.label.feed_url": "URL del feed",
"form.feed.label.feed_username": "Nome utente del feed",
"form.feed.label.fetch_via_proxy": "Recuperare tramite proxy",
"form.feed.label.fetch_via_proxy": "Usa il proxy configurato a livello di applicazione",
"form.feed.label.hide_globally": "Nascondere le voci nella lista globale dei non letti",
"form.feed.label.ignore_http_cache": "Ignora cache HTTP",
"form.feed.label.keeplist_rules": "Regole di autorizzazione",
@@ -187,6 +189,7 @@
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
"form.feed.label.ntfy_priority": "Ntfy priority",
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "Push entries to pushover.net",
"form.feed.label.pushover_default_priority": "Pushover default priority",
"form.feed.label.pushover_high_priority": "Pushover high priority",
@@ -421,6 +424,10 @@
"page.api_keys.table.last_used_at": "Ultimo uso",
"page.api_keys.table.token": "Gettone",
"page.api_keys.title": "Chiavi API",
"page.categories_count": [
"%d category",
"%d categories"
],
"page.categories.entries": "Articoli",
"page.categories.feed_count": [
"C'è %d feed.",
@@ -429,10 +436,6 @@
"page.categories.feeds": "Abbonamenti",
"page.categories.no_feed": "Nessun feed.",
"page.categories.title": "Categorie",
"page.categories_count": [
"%d category",
"%d categories"
],
"page.category_label": "Category: %s",
"page.edit_category.title": "Modifica categoria: %s",
"page.edit_feed.etag_header": "Header ETag:",
@@ -538,25 +541,25 @@
"page.settings.webauthn.passkeys": "Passkeys",
"page.settings.webauthn.register": "Registra la chiave di accesso",
"page.settings.webauthn.register.error": "Impossibile registrare la passkey",
"page.shared_entries.title": "Voci condivise",
"page.shared_entries_count": [
"%d shared entry",
"%d shared entries"
],
"page.starred.title": "Preferiti",
"page.shared_entries.title": "Voci condivise",
"page.starred_entry_count": [
"%d starred entry",
"%d starred entries"
],
"page.starred.title": "Preferiti",
"page.total_entry_count": [
"%d entry in total",
"%d entries in total"
],
"page.unread.title": "Da leggere",
"page.unread_entry_count": [
"%d unread entry",
"%d unread entries"
],
"page.unread.title": "Da leggere",
"page.users.actions": "Azioni",
"page.users.admin.no": "No",
"page.users.admin.yes": "Sì",
@@ -603,4 +606,4 @@
"time_elapsed.yesterday": "ieri",
"tooltip.keyboard_shortcuts": "Scorciatoia da tastiera: %s",
"tooltip.logged_user": "Autenticato come %s"
}
}
+12 -9
View File
@@ -70,10 +70,10 @@
"entry.state.loading": "読み込み中…",
"entry.state.saving": "保存中…",
"entry.status.mark_as_read": "既読にする",
"entry.status.mark_as_unread": "未読に戻す",
"entry.status.title": "記事の状態を変更",
"entry.status.toast.read": "既読にしました",
"entry.status.toast.unread": "未読にしました",
"entry.status.mark_as_unread": "未読に戻す",
"entry.tags.label": "タグ:",
"entry.unshare.label": "共有を解除",
"error.api_key_already_exists": "この API キーは既に存在します。",
@@ -115,6 +115,7 @@
"error.invalid_default_home_page": "デフォルトのトップページが無効です",
"error.invalid_display_mode": "Web アプリの表示モードが無効です。",
"error.invalid_entry_direction": "記事の表示順が無効です。",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "フィード URL が無効です。",
"error.invalid_gesture_nav": "ジェスチャー ナビゲーションが無効です。",
"error.invalid_language": "言語が無効です。",
@@ -126,6 +127,7 @@
"error.password_min_length": "パスワードは6文字以上である必要があります。",
"error.pocket_access_token": "Pocket の access token が取得できません!",
"error.pocket_request_token": "Pocket の request token が取得できません!",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
@@ -172,7 +174,7 @@
"form.feed.label.feed_password": "フィードのパスワード",
"form.feed.label.feed_url": "フィード URL",
"form.feed.label.feed_username": "フィードのユーザー名",
"form.feed.label.fetch_via_proxy": "プロキシ経由で取得",
"form.feed.label.fetch_via_proxy": "アプリケーションレベルで設定されたプロキシを使用する",
"form.feed.label.hide_globally": "未読一覧に記事を表示しない",
"form.feed.label.ignore_http_cache": "HTTPキャッシュを無視",
"form.feed.label.keeplist_rules": "Keep ルール",
@@ -185,6 +187,7 @@
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
"form.feed.label.ntfy_priority": "Ntfy priority",
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "Push entries to pushover.net",
"form.feed.label.pushover_default_priority": "Pushover default priority",
"form.feed.label.pushover_high_priority": "Pushover high priority",
@@ -419,6 +422,9 @@
"page.api_keys.table.last_used_at": "最終使用",
"page.api_keys.table.token": "トークン",
"page.api_keys.title": "API キー",
"page.categories_count": [
"%d category"
],
"page.categories.entries": "記事一覧",
"page.categories.feed_count": [
"%d 件のフィードがあります。"
@@ -426,9 +432,6 @@
"page.categories.feeds": "フィード一覧",
"page.categories.no_feed": "フィードはありません。",
"page.categories.title": "カテゴリ",
"page.categories_count": [
"%d category"
],
"page.category_label": "Category: %s",
"page.edit_category.title": "カテゴリを編集: %s",
"page.edit_feed.etag_header": "ETag ヘッダー:",
@@ -531,21 +534,21 @@
"page.settings.webauthn.passkeys": "Passkeys",
"page.settings.webauthn.register": "パスキーを登録する",
"page.settings.webauthn.register.error": "パスキーを登録できません",
"page.shared_entries.title": "共有エントリ",
"page.shared_entries_count": [
"%d shared entry"
],
"page.starred.title": "星付き",
"page.shared_entries.title": "共有エントリ",
"page.starred_entry_count": [
"%d starred entry"
],
"page.starred.title": "星付き",
"page.total_entry_count": [
"%d entry in total"
],
"page.unread.title": "未読",
"page.unread_entry_count": [
"%d unread entry"
],
"page.unread.title": "未読",
"page.users.actions": "アクション",
"page.users.admin.no": "非管理者",
"page.users.admin.yes": "管理者",
@@ -586,4 +589,4 @@
"time_elapsed.yesterday": "昨日",
"tooltip.keyboard_shortcuts": "キーボードショートカット: %s",
"tooltip.logged_user": "%s としてログイン中"
}
}
@@ -70,10 +70,10 @@
"entry.state.loading": "Tng leh chip-hêng…",
"entry.state.saving": "Tng leh pó-chûn…",
"entry.status.mark_as_read": "Chù chòe tha̍k kè",
"entry.status.mark_as_unread": "Chù chòe ah-bōe tha̍k",
"entry.status.title": "Kái chōng-thài",
"entry.status.toast.read": "Chù chòe tha̍k kè chòe soah",
"entry.status.toast.unread": "Chù chòe ah-bōe tha̍k chòe soah",
"entry.status.mark_as_unread": "Chù chòe ah-bōe tha̍k",
"entry.tags.label": "Khan-á:",
"entry.unshare.label": "Chhú-siau hun-hióng",
"error.api_key_already_exists": "Chit ê API só-sî í-keng chûn-chāi",
@@ -115,6 +115,7 @@
"error.invalid_default_home_page": "Ū-siat chú-ia̍h ū būn-tôe!",
"error.invalid_display_mode": "Ū būn-tôe ê su-li̍p bô͘-sek.",
"error.invalid_entry_direction": "Ū būn-tôe ê su-li̍p hong-hiòng.",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "Beh tēng ê siau-sit lâi-goân ê bāng-chí ū būn-tôe.",
"error.invalid_gesture_nav": "Chhiú-sè tō-lám ū būn-tôe.",
"error.invalid_language": "Ū būn-tôe ê gú-giân.",
@@ -126,6 +127,7 @@
"error.password_min_length": "Chhiáⁿ chì-chió ài su-li̍p la̍k ê lī goân.",
"error.pocket_access_token": "Bô-hoat-tō͘ ùi Pocket thê tio̍h access token",
"error.pocket_request_token": "Bô-hoat-tō͘ ùi Pocket thê tio̍h request token",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"error.settings_block_rule_fieldname_invalid": "Bô-hāu ê hong-só kui-chek: kui-chek #%d khiàm ū-hāu ê lân-ūi miâ (e-sai ê soán-hāng: %s)",
"error.settings_block_rule_invalid_regex": "Bô-hāu ê hong-só kui-chek: kui-chek #%d ê bô͘-sek m̄ sī ha̍p-hoat ê chiàⁿ-kui piáu-ta̍t sek",
"error.settings_block_rule_regex_required": "Bô-hāu ê hong-só kui-chek: kui-chek #%d bô thê-kiong chiàⁿ-kui piáu-ta̍t sek",
@@ -172,7 +174,7 @@
"form.feed.label.feed_password": "Siau-sit lâi-goân bi̍t-bé",
"form.feed.label.feed_url": "Siau-sit lâi-goân bāng-chí",
"form.feed.label.feed_username": "Siau-sit lâi-goân kháu-chō miâ",
"form.feed.label.fetch_via_proxy": "Thàu-kè tāi-lí lia̍h",
"form.feed.label.fetch_via_proxy": "Iōng tī su-hāu-khì siat-tēng ê proxy",
"form.feed.label.hide_globally": "Tī choân-he̍k ah-bōe tha̍k--ê lia̍t-pió am-khàm siau-sit",
"form.feed.label.ignore_http_cache": "Pàng-ba̍k HTTP cache",
"form.feed.label.keeplist_rules": "Pó-liû kui-chek",
@@ -185,6 +187,7 @@
"form.feed.label.ntfy_min_priority": "Ntfy siōng kē iu-sian sūn-sū",
"form.feed.label.ntfy_priority": "Ntfy iu-sian sūn-sū",
"form.feed.label.ntfy_topic": "Ntfy topic (soán thiⁿ)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "Push entries to Pushover",
"form.feed.label.pushover_default_priority": "Default priority",
"form.feed.label.pushover_high_priority": "High priority",
@@ -419,6 +422,9 @@
"page.api_keys.table.last_used_at": "Siōng-bóe pái sú-iōng",
"page.api_keys.table.token": "Só-sî",
"page.api_keys.title": "API só-sî",
"page.categories_count": [
"%d ê lūi-pia̍t"
],
"page.categories.entries": "Siau-sit",
"page.categories.feed_count": [
"Ū %d ê Siau-sit lâi-goân"
@@ -426,9 +432,6 @@
"page.categories.feeds": "Siau-sit lâi-goân",
"page.categories.no_feed": "Ah-bô siau-sit lâi-goân",
"page.categories.title": "Lūi-pia̍t",
"page.categories_count": [
"%d ê lūi-pia̍t"
],
"page.category_label": "Lūi-pia̍t: %s",
"page.edit_category.title": "Pian-chi̍p lūi-pia̍t: %s",
"page.edit_feed.etag_header": "ETag piau-thâu:",
@@ -531,21 +534,21 @@
"page.settings.webauthn.passkeys": "Passkeys",
"page.settings.webauthn.register": "Chù-chheh Passkey",
"page.settings.webauthn.register.error": "Bô-hoat-tō͘ chù-chheh Passkey",
"page.shared_entries.title": "Hun-hióng kè ê siau-sit",
"page.shared_entries_count": [
"Í-keng hun-hióng %d ê siau-sit"
],
"page.starred.title": "Siu-chông",
"page.shared_entries.title": "Hun-hióng kè ê siau-sit",
"page.starred_entry_count": [
"%d ê siu-chông ê siau-sit"
],
"page.starred.title": "Siu-chông",
"page.total_entry_count": [
"Lóng-chóng %d ê siau-sit"
],
"page.unread.title": "Ah-bōe tha̍k",
"page.unread_entry_count": [
"%d ê siau-sit ah-bōe tha̍k"
],
"page.unread.title": "Ah-bōe tha̍k",
"page.users.actions": "chhau-chok",
"page.users.admin.no": "Hóⁿ",
"page.users.admin.yes": "Sī",
@@ -586,4 +589,4 @@
"time_elapsed.yesterday": "cha-hng",
"tooltip.keyboard_shortcuts": "Khoài-sok khí:%s",
"tooltip.logged_user": "Chit-má teng-lo̍k--ê: %s"
}
}
+13 -10
View File
@@ -72,10 +72,10 @@
"entry.state.loading": "Laden...",
"entry.state.saving": "Opslaan...",
"entry.status.mark_as_read": "Markeren als gelezen",
"entry.status.mark_as_unread": "Markeren als ongelezen",
"entry.status.title": "Verander artikelstatus",
"entry.status.toast.read": "Gemarkeerd als gelezen",
"entry.status.toast.unread": "Gemarkeerd als ongelezen",
"entry.status.mark_as_unread": "Markeren als ongelezen",
"entry.tags.label": "Tags:",
"entry.unshare.label": "Delen ongedaan maken",
"error.api_key_already_exists": "Deze API-sleutel bestaat al.",
@@ -117,6 +117,7 @@
"error.invalid_default_home_page": "Ongeldige startpagina!",
"error.invalid_display_mode": "Ongeldige weergavemodus voor de webapp.",
"error.invalid_entry_direction": "Ongeldige sorteervolgorde.",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "Ongeldige feed URL.",
"error.invalid_gesture_nav": "Ongeldige gebarennavigatie.",
"error.invalid_language": "Ongeldige taal.",
@@ -128,6 +129,7 @@
"error.password_min_length": "Minimaal 6 tekens gebruiken.",
"error.pocket_access_token": "Kon geen toegangstoken ophalen van Pocket!",
"error.pocket_request_token": "Kon geen aanvraagtoken ophalen van Pocket!",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"error.settings_block_rule_fieldname_invalid": "Ongeldige blokkeerregel: regel #%d mist een geldige veldnaam (Opties: %s)",
"error.settings_block_rule_invalid_regex": "Ongeldige blokkeerregel: het patroon van regel #%d is geen geldige regex",
"error.settings_block_rule_regex_required": "Ongeldige blokkeerregel: het patroon van regel #%d is niet opgegeven",
@@ -174,7 +176,7 @@
"form.feed.label.feed_password": "Feed wachtwoord",
"form.feed.label.feed_url": "Feed URL",
"form.feed.label.feed_username": "Feed gebruikersnaam",
"form.feed.label.fetch_via_proxy": "Ophalen via proxy",
"form.feed.label.fetch_via_proxy": "Gebruik de proxy die op applicatieniveau is geconfigureerd",
"form.feed.label.hide_globally": "Verberg artikelen in de globale ongelezen lijst",
"form.feed.label.ignore_http_cache": "Negeer HTTP-cache",
"form.feed.label.keeplist_rules": "Bewaarregels",
@@ -187,6 +189,7 @@
"form.feed.label.ntfy_min_priority": "Ntfy minimale prioriteit",
"form.feed.label.ntfy_priority": "Ntfy prioriteit",
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "Push entries to pushover.net",
"form.feed.label.pushover_default_priority": "Pushover default priority",
"form.feed.label.pushover_high_priority": "Pushover high priority",
@@ -421,6 +424,10 @@
"page.api_keys.table.last_used_at": "Laatst gebruikt",
"page.api_keys.table.token": "Token",
"page.api_keys.title": "API-sleutels",
"page.categories_count": [
"%d categorie",
"%d categorieën"
],
"page.categories.entries": "Artikelen",
"page.categories.feed_count": [
"Er is %d feed.",
@@ -429,10 +436,6 @@
"page.categories.feeds": "Feeds",
"page.categories.no_feed": "Geen feed.",
"page.categories.title": "Categorieën",
"page.categories_count": [
"%d categorie",
"%d categorieën"
],
"page.category_label": "Categorie: %s",
"page.edit_category.title": "Bewerk categorie: %s",
"page.edit_feed.etag_header": "ETAG header:",
@@ -538,25 +541,25 @@
"page.settings.webauthn.passkeys": "Passkeys",
"page.settings.webauthn.register": "Passkey registreren",
"page.settings.webauthn.register.error": "Kan passkey niet registreren",
"page.shared_entries.title": "Gedeelde artikelen",
"page.shared_entries_count": [
"%d gedeeld artikel",
"%d gedeelde artikelen"
],
"page.starred.title": "Favorieten",
"page.shared_entries.title": "Gedeelde artikelen",
"page.starred_entry_count": [
"%d favoriet artikel",
"%d favoriete artikelen"
],
"page.starred.title": "Favorieten",
"page.total_entry_count": [
"%d artikel totaal",
"%d artikelen totaal"
],
"page.unread.title": "Ongelezen",
"page.unread_entry_count": [
"%d ongelezen artikel",
"%d ongelezen artikelen"
],
"page.unread.title": "Ongelezen",
"page.users.actions": "Acties",
"page.users.admin.no": "Nee",
"page.users.admin.yes": "Ja",
@@ -603,4 +606,4 @@
"time_elapsed.yesterday": "gisteren",
"tooltip.keyboard_shortcuts": "Sneltoets: %s",
"tooltip.logged_user": "Ingelogd als %s"
}
}
+14 -11
View File
@@ -74,10 +74,10 @@
"entry.state.loading": "Ładowanie…",
"entry.state.saving": "Zapisywanie…",
"entry.status.mark_as_read": "Oznacz jako przeczytany",
"entry.status.mark_as_unread": "Oznacz jako nieprzeczytany",
"entry.status.title": "Zmień status wpisu",
"entry.status.toast.read": "Oznaczono jako przeczytany",
"entry.status.toast.unread": "Oznaczono jako nieprzeczytany",
"entry.status.mark_as_unread": "Oznacz jako nieprzeczytany",
"entry.tags.label": "Znaczniki:",
"entry.unshare.label": "Cofnij udostępnianie",
"error.api_key_already_exists": "Ten klucz API już istnieje.",
@@ -119,6 +119,7 @@
"error.invalid_default_home_page": "Nieprawidłowa domyślna strona główna!",
"error.invalid_display_mode": "Nieprawidłowy tryb wyświetlania aplikacji sieciowej.",
"error.invalid_entry_direction": "Nieprawidłowa kolejność sortowania.",
"error.invalid_feed_proxy_url": "Nieprawidłowy adres URL serwera proxy.",
"error.invalid_feed_url": "Nieprawidłowy adres URL kanału.",
"error.invalid_gesture_nav": "Nieprawidłowa nawigacja gestami.",
"error.invalid_language": "Nieprawidłowy język.",
@@ -130,6 +131,7 @@
"error.password_min_length": "Musisz użyć co najmniej 6 znaków.",
"error.pocket_access_token": "Nie można pobrać tokena dostępu z Pocket!",
"error.pocket_request_token": "Nie można pobrać tokena żądania z Pocket!",
"error.proxy_url_not_empty": "Adres URL serwera proxy nie może być pusty.",
"error.settings_block_rule_fieldname_invalid": "Nieprawidłowa reguła blokowania: w regule #%d brakuje prawidłowej nazwy pola (opcje: %s)",
"error.settings_block_rule_invalid_regex": "Nieprawidłowa reguła blokowania: wzór reguły #%d nie jest prawidłowym wyrażeniem regularnym",
"error.settings_block_rule_regex_required": "Nieprawidłowa reguła blokowania: nie podano wzorca reguły #%d",
@@ -176,7 +178,7 @@
"form.feed.label.feed_password": "Hasło do subskrypcji",
"form.feed.label.feed_url": "Adres URL kanału",
"form.feed.label.feed_username": "Nazwa użytkownika subskrypcji",
"form.feed.label.fetch_via_proxy": "Pobierz przez proxy",
"form.feed.label.fetch_via_proxy": "Użyj serwera proxy skonfigurowanego na poziomie aplikacji",
"form.feed.label.hide_globally": "Ukryj wpisy na globalnej liście nieprzeczytanych",
"form.feed.label.ignore_http_cache": "Zignoruj pamięć podręczną HTTP",
"form.feed.label.keeplist_rules": "Reguły utrzymywania",
@@ -189,6 +191,7 @@
"form.feed.label.ntfy_min_priority": "Minimalny priorytet ntfy",
"form.feed.label.ntfy_priority": "Priorytet ntfy",
"form.feed.label.ntfy_topic": "Temat ntfy (opcjonalny)",
"form.feed.label.proxy_url": "Adres URL serwera proxy",
"form.feed.label.pushover_activate": "Prześlij wpisy do pushover.net",
"form.feed.label.pushover_default_priority": "Domyślny priorytet Pushover",
"form.feed.label.pushover_high_priority": "Wysoki priorytet Pushover",
@@ -423,6 +426,11 @@
"page.api_keys.table.last_used_at": "Ostatnio używane",
"page.api_keys.table.token": "Token",
"page.api_keys.title": "Klucze API",
"page.categories_count": [
"%d kategoria",
"%d kategorie",
"%d kategorii"
],
"page.categories.entries": "Wpisy",
"page.categories.feed_count": [
"Jest %d kanał.",
@@ -432,11 +440,6 @@
"page.categories.feeds": "Kanały",
"page.categories.no_feed": "Brak kanałów.",
"page.categories.title": "Kategorie",
"page.categories_count": [
"%d kategoria",
"%d kategorie",
"%d kategorii"
],
"page.category_label": "Kategoria: %s",
"page.edit_category.title": "Edytuj kategorię: %s",
"page.edit_feed.etag_header": "Nagłówek ETag:",
@@ -545,29 +548,29 @@
"page.settings.webauthn.passkeys": "Klucze dostępu",
"page.settings.webauthn.register": "Zarejestruj klucz dostępu",
"page.settings.webauthn.register.error": "Nie można zarejestrować klucza dostępu",
"page.shared_entries.title": "Udostępnione wpisy",
"page.shared_entries_count": [
"%d udostępniony wpis",
"%d udostępnione wpisy",
"%d udostępnionych wpisów"
],
"page.starred.title": "Ulubione",
"page.shared_entries.title": "Udostępnione wpisy",
"page.starred_entry_count": [
"%d ulubiony wpis",
"%d ulubione wpisy",
"%d ulubionych wpisów"
],
"page.starred.title": "Ulubione",
"page.total_entry_count": [
"%d wpis łącznie",
"%d wpisy łącznie",
"%d wpisów łącznie"
],
"page.unread.title": "Nieprzeczytane",
"page.unread_entry_count": [
"%d nieprzeczytany wpis",
"%d nieprzeczytane wpisy",
"%d nieprzeczytanych wpisów"
],
"page.unread.title": "Nieprzeczytane",
"page.users.actions": "Działania",
"page.users.admin.no": "Nie",
"page.users.admin.yes": "Tak",
@@ -620,4 +623,4 @@
"time_elapsed.yesterday": "wczoraj",
"tooltip.keyboard_shortcuts": "Skróty klawiszowe: %s",
"tooltip.logged_user": "Zalogowany jako %s"
}
}
+13 -10
View File
@@ -72,10 +72,10 @@
"entry.state.loading": "Carregando...",
"entry.state.saving": "Salvando...",
"entry.status.mark_as_read": "Marcar como lido",
"entry.status.mark_as_unread": "Marcar como não lido",
"entry.status.title": "Modificar estado deste item",
"entry.status.toast.read": "Marcado como lido",
"entry.status.toast.unread": "Marcado como não lido",
"entry.status.mark_as_unread": "Marcar como não lido",
"entry.tags.label": "Etiquetas:",
"entry.unshare.label": "Descompartilhar",
"error.api_key_already_exists": "Essa chave de API já existe.",
@@ -117,6 +117,7 @@
"error.invalid_default_home_page": "Página inicial por defeito inválida!",
"error.invalid_display_mode": "Modo de exibição de aplicativo inválido da web.",
"error.invalid_entry_direction": "Direção de entrada inválida.",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "URL de feed inválido.",
"error.invalid_gesture_nav": "Navegação por gestos inválida.",
"error.invalid_language": "Idioma inválido.",
@@ -128,6 +129,7 @@
"error.password_min_length": "A senha deve ter no mínimo 6 caracteres.",
"error.pocket_access_token": "Não foi possível obter um token de acesso no Pocket!",
"error.pocket_request_token": "Não foi possível obter um pedido de token no Pocket!",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
@@ -174,7 +176,7 @@
"form.feed.label.feed_password": "Senha da fonte",
"form.feed.label.feed_url": "URL da fonte",
"form.feed.label.feed_username": "Nome de usuário da fonte",
"form.feed.label.fetch_via_proxy": "Buscar via proxy",
"form.feed.label.fetch_via_proxy": "Usar o proxy configurado no nível da aplicação",
"form.feed.label.hide_globally": "Ocultar entradas na lista global não lida",
"form.feed.label.ignore_http_cache": "Ignorar cache HTTP",
"form.feed.label.keeplist_rules": "Regras de permissão",
@@ -187,6 +189,7 @@
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
"form.feed.label.ntfy_priority": "Ntfy priority",
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "Push entries to pushover.net",
"form.feed.label.pushover_default_priority": "Pushover default priority",
"form.feed.label.pushover_high_priority": "Pushover high priority",
@@ -421,6 +424,10 @@
"page.api_keys.table.last_used_at": "Ultima utilização",
"page.api_keys.table.token": "Token",
"page.api_keys.title": "Chaves de API",
"page.categories_count": [
"%d category",
"%d categories"
],
"page.categories.entries": "Itens",
"page.categories.feed_count": [
"Existe %d fonte.",
@@ -429,10 +436,6 @@
"page.categories.feeds": "Inscrições",
"page.categories.no_feed": "Sem fonte.",
"page.categories.title": "Categorias",
"page.categories_count": [
"%d category",
"%d categories"
],
"page.category_label": "Category: %s",
"page.edit_category.title": "Editar categoria: %s",
"page.edit_feed.etag_header": "Cabeçalho 'ETag':",
@@ -538,25 +541,25 @@
"page.settings.webauthn.passkeys": "Passkeys",
"page.settings.webauthn.register": "Registrar senha",
"page.settings.webauthn.register.error": "Não foi possível registrar a senha",
"page.shared_entries.title": "Itens compartilhados",
"page.shared_entries_count": [
"%d shared entry",
"%d shared entries"
],
"page.starred.title": "Favoritos",
"page.shared_entries.title": "Itens compartilhados",
"page.starred_entry_count": [
"%d starred entry",
"%d starred entries"
],
"page.starred.title": "Favoritos",
"page.total_entry_count": [
"%d entry in total",
"%d entries in total"
],
"page.unread.title": "Não lidos",
"page.unread_entry_count": [
"%d unread entry",
"%d unread entries"
],
"page.unread.title": "Não lidos",
"page.users.actions": "Ações",
"page.users.admin.no": "Não",
"page.users.admin.yes": "Sim",
@@ -603,4 +606,4 @@
"time_elapsed.yesterday": "ontem",
"tooltip.keyboard_shortcuts": "Atalho do teclado: %s",
"tooltip.logged_user": "Autenticado como %s"
}
}
+5 -2
View File
@@ -74,10 +74,10 @@
"entry.state.loading": "Încarc…",
"entry.state.saving": "Salvez…",
"entry.status.mark_as_read": "Marcați ca citit",
"entry.status.mark_as_unread": "Marcați ca necitit",
"entry.status.title": "Modifică starea intrării",
"entry.status.toast.read": "Marcat ca citit",
"entry.status.toast.unread": "Marcat ca necitit",
"entry.status.mark_as_unread": "Marcați ca necitit",
"entry.tags.label": "Etichete:",
"entry.unshare.label": "Elimină partajarea",
"error.api_key_already_exists": "Această cheie API există deja.",
@@ -119,6 +119,7 @@
"error.invalid_default_home_page": "Pagină de start invalidă!",
"error.invalid_display_mode": "Mod invalid de afișare în aplicația web.",
"error.invalid_entry_direction": "Direcție invalidă ăn intrare.",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "Adresa URL a fluxului este invalidă.",
"error.invalid_gesture_nav": "Gest de navigare invalid.",
"error.invalid_language": "Limbă invalidă.",
@@ -130,6 +131,7 @@
"error.password_min_length": "Parola trebuie să aibă cel puțin 6 caractere.",
"error.pocket_access_token": "Nu poate obține token-ul de acces de la Pocket!",
"error.pocket_request_token": "Nu poate obține token-ul solicitat de la Pocket!",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"error.settings_block_rule_fieldname_invalid": "Regulă de bloc invalidă: regulii #%d îi lipsește un nume valid de câmp (Opțiuni: %s)",
"error.settings_block_rule_invalid_regex": "Regulă de bloc invalidă: modelul regulii #%d's nu este regex valid",
"error.settings_block_rule_regex_required": "Regulă de bloc invalidă: modelul regulii #%d's nu este furnizat",
@@ -176,7 +178,7 @@
"form.feed.label.feed_password": "Parolă Flux",
"form.feed.label.feed_url": "Flux URL",
"form.feed.label.feed_username": "Nume user Flux",
"form.feed.label.fetch_via_proxy": "Actualizare prin proxy",
"form.feed.label.fetch_via_proxy": "Utilizați proxy-ul configurat la nivelul aplicației",
"form.feed.label.hide_globally": "Ascunde intrările în lista globală de articole necitite",
"form.feed.label.ignore_http_cache": "Ignoră cache HTTP",
"form.feed.label.keeplist_rules": "Reguli de Păstrare",
@@ -189,6 +191,7 @@
"form.feed.label.ntfy_min_priority": "Prioritate minimă Ntfy",
"form.feed.label.ntfy_priority": "Prioritate Ntfy",
"form.feed.label.ntfy_topic": "Subiect Ntfy (opțional)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "Activează Pushover",
"form.feed.label.pushover_default_priority": "Prioritate implicită Pushover",
"form.feed.label.pushover_high_priority": "Prioritate ridicată Pushover",
+14 -11
View File
@@ -74,10 +74,10 @@
"entry.state.loading": "Загрузка…",
"entry.state.saving": "Сохранение…",
"entry.status.mark_as_read": "Отметить как прочитанное",
"entry.status.mark_as_unread": "Пометить как непрочитанное",
"entry.status.title": "Изменить статус записи",
"entry.status.toast.read": "Помечено как прочитанное",
"entry.status.toast.unread": "Помечено как непрочитанное",
"entry.status.mark_as_unread": "Пометить как непрочитанное",
"entry.tags.label": "Теги:",
"entry.unshare.label": "Удалить из общедоступных",
"error.api_key_already_exists": "Этот API-ключ уже существует.",
@@ -119,6 +119,7 @@
"error.invalid_default_home_page": "Недопустимая домашняя страница по умолчанию!",
"error.invalid_display_mode": "Недопустимый режим отображения веб-приложения.",
"error.invalid_entry_direction": "Недопустимая сортировка записей.",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "Недействительная ссылка подписки.",
"error.invalid_gesture_nav": "Недопустимая навигация жестами.",
"error.invalid_language": "Недопустимый язык.",
@@ -130,6 +131,7 @@
"error.password_min_length": "Вы должны использовать минимум 6 символов.",
"error.pocket_access_token": "Не удалось получить ключ доступа от Pocket!",
"error.pocket_request_token": "Не удалось получить request token от Pocket!",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
@@ -176,7 +178,7 @@
"form.feed.label.feed_password": "Пароль подписки",
"form.feed.label.feed_url": "Адрес подписки",
"form.feed.label.feed_username": "Имя пользователя подписки",
"form.feed.label.fetch_via_proxy": "Использовать прокси",
"form.feed.label.fetch_via_proxy": "Использовать прокси, настроенный на уровне приложения",
"form.feed.label.hide_globally": "Скрыть записи в глобальном списке непрочитанных",
"form.feed.label.ignore_http_cache": "Игнорировать HTTP кеш",
"form.feed.label.keeplist_rules": "Правила белого списка",
@@ -189,6 +191,7 @@
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
"form.feed.label.ntfy_priority": "Ntfy priority",
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "Push entries to pushover.net",
"form.feed.label.pushover_default_priority": "Pushover default priority",
"form.feed.label.pushover_high_priority": "Pushover high priority",
@@ -423,6 +426,11 @@
"page.api_keys.table.last_used_at": "Последнее использование",
"page.api_keys.table.token": "Токен",
"page.api_keys.title": "API-ключи",
"page.categories_count": [
"%d category",
"%d categories",
"%d categories"
],
"page.categories.entries": "Cтатьи",
"page.categories.feed_count": [
"Есть %d подписка.",
@@ -432,11 +440,6 @@
"page.categories.feeds": "Подписки",
"page.categories.no_feed": "Нет подписок.",
"page.categories.title": "Категории",
"page.categories_count": [
"%d category",
"%d categories",
"%d categories"
],
"page.category_label": "Category: %s",
"page.edit_category.title": "Изменить категорию: %s",
"page.edit_feed.etag_header": "Заголовок ETag:",
@@ -545,29 +548,29 @@
"page.settings.webauthn.passkeys": "Passkeys",
"page.settings.webauthn.register": "Зарегистрировать пароль",
"page.settings.webauthn.register.error": "Не удается зарегистрировать пароль",
"page.shared_entries.title": "Общедоступные статьи",
"page.shared_entries_count": [
"%d shared entry",
"%d shared entries",
"%d shared entries"
],
"page.starred.title": "Избранное",
"page.shared_entries.title": "Общедоступные статьи",
"page.starred_entry_count": [
"%d starred entry",
"%d starred entries",
"%d starred entries"
],
"page.starred.title": "Избранное",
"page.total_entry_count": [
"%d entry in total",
"%d entries in total",
"%d entries in total"
],
"page.unread.title": "Непрочитанное",
"page.unread_entry_count": [
"%d unread entry",
"%d unread entries",
"%d unread entries"
],
"page.unread.title": "Непрочитанное",
"page.users.actions": "Действия",
"page.users.admin.no": "Нет",
"page.users.admin.yes": "Да",
@@ -620,4 +623,4 @@
"time_elapsed.yesterday": "вчера",
"tooltip.keyboard_shortcuts": "Сочетания клавиш: %s",
"tooltip.logged_user": "Авторизован как %s"
}
}
+13 -10
View File
@@ -72,10 +72,10 @@
"entry.state.loading": "Yükleniyor...",
"entry.state.saving": "Kaydediliyor...",
"entry.status.mark_as_read": "Okundu olarak işaretle",
"entry.status.mark_as_unread": "Okunmadı olarak işaretle",
"entry.status.title": "Makele okundu durumunu değiştir",
"entry.status.toast.read": "Okundu olarak işaretlendi",
"entry.status.toast.unread": "Okunmamış olarak işaretlendi",
"entry.status.mark_as_unread": "Okunmadı olarak işaretle",
"entry.tags.label": "Etiketler:",
"entry.unshare.label": "Paylaşma",
"error.api_key_already_exists": "Bu API anahtarı zaten mevcut.",
@@ -117,6 +117,7 @@
"error.invalid_default_home_page": "Geçersiz varsayılan ana sayfa!",
"error.invalid_display_mode": "Geçersiz web uygulaması görüntüleme modu.",
"error.invalid_entry_direction": "Geçersiz makele sıralaması.",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "Geçersiz besleme URL'si.",
"error.invalid_gesture_nav": "Hareketle gezinme geçersiz.",
"error.invalid_language": "Geçersiz dil.",
@@ -128,6 +129,7 @@
"error.password_min_length": "Parola en az 6 karakter içermeli.",
"error.pocket_access_token": "Pocket'tan access tokeni alınamıyor!",
"error.pocket_request_token": "Pocket'tan request tokeni alınamıyor!",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"error.settings_block_rule_fieldname_invalid": "Geçersiz Engelleme kuralı: #%d kuralında geçerli bir alan adı eksik (Seçenekler: %s)",
"error.settings_block_rule_invalid_regex": "Geçersiz Engelleme kuralı: #%d kuralı modeli geçerli bir düzenli ifade değil",
"error.settings_block_rule_regex_required": "Geçersiz Engelleme kuralı: #%d kuralı modeli sağlanmadı",
@@ -174,7 +176,7 @@
"form.feed.label.feed_password": "Besleme Parolası",
"form.feed.label.feed_url": "Besleme URL'si",
"form.feed.label.feed_username": "Besleme Kullanıcı Adı",
"form.feed.label.fetch_via_proxy": "Proxy ile çek",
"form.feed.label.fetch_via_proxy": "Uygulama düzeyinde yapılandırılmış proxy'yi kullan",
"form.feed.label.hide_globally": "Genel okunmamış listesindeki girişleri gizle",
"form.feed.label.ignore_http_cache": "HTTP önbelleğini yoksay",
"form.feed.label.keeplist_rules": "Saklama Kuralları",
@@ -187,6 +189,7 @@
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
"form.feed.label.ntfy_priority": "Ntfy priority",
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "Push entries to pushover.net",
"form.feed.label.pushover_default_priority": "Pushover default priority",
"form.feed.label.pushover_high_priority": "Pushover high priority",
@@ -421,6 +424,10 @@
"page.api_keys.table.last_used_at": "Son Kullanılma",
"page.api_keys.table.token": "Token",
"page.api_keys.title": "API Anahtarları",
"page.categories_count": [
"%d kategori",
"%d kategori"
],
"page.categories.entries": "Makaleler",
"page.categories.feed_count": [
"%d besleme var.",
@@ -429,10 +436,6 @@
"page.categories.feeds": "Beslemeler",
"page.categories.no_feed": "Besleme yok.",
"page.categories.title": "Kategoriler",
"page.categories_count": [
"%d kategori",
"%d kategori"
],
"page.category_label": "Kategori: %s",
"page.edit_category.title": "Kategoriyi Düzenle: %s",
"page.edit_feed.etag_header": "ETag başlığı:",
@@ -538,25 +541,25 @@
"page.settings.webauthn.passkeys": "Passkeyler",
"page.settings.webauthn.register": "Passkey'i kaydet",
"page.settings.webauthn.register.error": "Passkey kaydedilemiyor",
"page.shared_entries.title": "Paylaşılan makaleler",
"page.shared_entries_count": [
"%d paylaşılan makaleler",
"%d paylaşılan makaleler"
],
"page.starred.title": "Yıldızlı",
"page.shared_entries.title": "Paylaşılan makaleler",
"page.starred_entry_count": [
"%d yıldızlanmış makale",
"%d yıldızlanmış makale"
],
"page.starred.title": "Yıldızlı",
"page.total_entry_count": [
"Toplamda %d makale",
"Toplamda %d makale"
],
"page.unread.title": "Okunmadı",
"page.unread_entry_count": [
"Toplamda %d okunmamış makale",
"Toplamda %d okunmamış makale"
],
"page.unread.title": "Okunmadı",
"page.users.actions": "Eylemler",
"page.users.admin.no": "Hayır",
"page.users.admin.yes": "Evet",
@@ -603,4 +606,4 @@
"time_elapsed.yesterday": "dün",
"tooltip.keyboard_shortcuts": "Klavye Kısayolu: %s",
"tooltip.logged_user": "%s olarak giriş yapıldı"
}
}
+14 -11
View File
@@ -74,10 +74,10 @@
"entry.state.loading": "Завантаження...",
"entry.state.saving": "Зберігаю...",
"entry.status.mark_as_read": "Позначити як прочитане",
"entry.status.mark_as_unread": "Позначити як непрочитане",
"entry.status.title": "Змінити стан запису",
"entry.status.toast.read": "Відмічено прочитаним",
"entry.status.toast.unread": "Відмічено непрочитаним",
"entry.status.mark_as_unread": "Позначити як непрочитане",
"entry.tags.label": "Теги:",
"entry.unshare.label": "Не ділитися",
"error.api_key_already_exists": "Такий ключ API вже існує.",
@@ -119,6 +119,7 @@
"error.invalid_default_home_page": "Недійсна домашня сторінка за замовчуванням!",
"error.invalid_display_mode": "Недійсний режим відображення.",
"error.invalid_entry_direction": "Недійсний напрямок запису.",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "Недійсна URL-адреса стрічки.",
"error.invalid_gesture_nav": "Недійсна навігація жестами.",
"error.invalid_language": "Недійсна мова.",
@@ -130,6 +131,7 @@
"error.password_min_length": "Пароль має складати щонайменше 6 символів.",
"error.pocket_access_token": "Не вдалося отримати токен доступу з Pocket!",
"error.pocket_request_token": "Не вдалося отримати токен доступу з Pocket!",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
@@ -176,7 +178,7 @@
"form.feed.label.feed_password": "Пароль для завантаження",
"form.feed.label.feed_url": "URL-адреса стрічки",
"form.feed.label.feed_username": "Ім’я користувача для завантаження",
"form.feed.label.fetch_via_proxy": "Використати проксі-сервер",
"form.feed.label.fetch_via_proxy": "Використовувати проксі, налаштований на рівні програми",
"form.feed.label.hide_globally": "Приховати записи в глобальному списку непрочитаного",
"form.feed.label.ignore_http_cache": "Ігнорувати кеш HTTP",
"form.feed.label.keeplist_rules": "Правила дозволення",
@@ -189,6 +191,7 @@
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
"form.feed.label.ntfy_priority": "Ntfy priority",
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "Push entries to pushover.net",
"form.feed.label.pushover_default_priority": "Pushover default priority",
"form.feed.label.pushover_high_priority": "Pushover high priority",
@@ -423,6 +426,11 @@
"page.api_keys.table.last_used_at": "Дата останнього використання",
"page.api_keys.table.token": "Токен",
"page.api_keys.title": "Ключі API",
"page.categories_count": [
"%d category",
"%d categories",
"%d categories"
],
"page.categories.entries": "Статті",
"page.categories.feed_count": [
"Містить %d стрічку.",
@@ -432,11 +440,6 @@
"page.categories.feeds": "Підписки",
"page.categories.no_feed": "Немає стрічки.",
"page.categories.title": "Категорії",
"page.categories_count": [
"%d category",
"%d categories",
"%d categories"
],
"page.category_label": "Категорія: %s",
"page.edit_category.title": "Редагування категорії: %s",
"page.edit_feed.etag_header": "Заголовок ETag:",
@@ -545,29 +548,29 @@
"page.settings.webauthn.passkeys": "Passkeys",
"page.settings.webauthn.register": "Зареєструвати пароль",
"page.settings.webauthn.register.error": "Не вдалося зареєструвати ключ доступу",
"page.shared_entries.title": "Спільні записи",
"page.shared_entries_count": [
"%d shared entry",
"%d shared entries",
"%d shared entries"
],
"page.starred.title": "З зірочкою",
"page.shared_entries.title": "Спільні записи",
"page.starred_entry_count": [
"%d starred entry",
"%d starred entries",
"%d starred entries"
],
"page.starred.title": "З зірочкою",
"page.total_entry_count": [
"%d entry in total",
"%d entries in total",
"%d entries in total"
],
"page.unread.title": "Непрочитане",
"page.unread_entry_count": [
"%d unread entry",
"%d unread entries",
"%d unread entries"
],
"page.unread.title": "Непрочитане",
"page.users.actions": "Дії",
"page.users.admin.no": "Ні",
"page.users.admin.yes": "Так",
@@ -620,4 +623,4 @@
"time_elapsed.yesterday": "вчора",
"tooltip.keyboard_shortcuts": "Комбінація клавіш: %s",
"tooltip.logged_user": "Здійснено вхід як %s"
}
}
+57 -54
View File
@@ -13,7 +13,7 @@
"action.update": "更新",
"alert.account_linked": "您的外部账号已关联!",
"alert.account_unlinked": "您的外部帐户现已解除关联!",
"alert.background_feed_refresh": "所有订阅源在后台刷新中。您可以继续使用Miniflux,同时此过程正在运行。",
"alert.background_feed_refresh": "所有订阅源在后台更新。此过程中您仍可继续使用 Miniflux。",
"alert.feed_error": "该源存在问题",
"alert.no_bookmark": "目前没有收藏",
"alert.no_category": "目前没有分类",
@@ -70,10 +70,10 @@
"entry.state.loading": "载入中…",
"entry.state.saving": "保存中…",
"entry.status.mark_as_read": "标为已读",
"entry.status.mark_as_unread": "标为未读",
"entry.status.title": "更改状态",
"entry.status.toast.read": "已标为已读",
"entry.status.toast.unread": "已标为未读",
"entry.status.mark_as_unread": "标为未读",
"entry.tags.label": "标签:",
"entry.unshare.label": "取消分享",
"error.api_key_already_exists": "此 API 密钥已存在。",
@@ -98,34 +98,36 @@
"error.feed_title_not_empty": "订阅源的标题不能为空。",
"error.feed_url_not_empty": "订阅源的网址不能为空。",
"error.fields_mandatory": "必须填写全部信息",
"error.http_bad_gateway": "当前由于错误的网关导致该网站无法访问,问题不在Miniflux,请稍后重试。",
"error.http_bad_gateway": "当前由于错误的网关导致该网站无法访问,问题不在 Miniflux,请稍后重试。",
"error.http_body_read": "无法读取HTTP主体: %v。",
"error.http_client_error": "HTTP 客户端错误r: %v。",
"error.http_empty_response": "HTTP响应内容为空,该网站可能正在使用机器人保护机制。",
"error.http_empty_response_body": "HTTP响应主体为空。",
"error.http_empty_response": "HTTP 响应内容为空,该网站可能正在使用机器人保护机制。",
"error.http_empty_response_body": "HTTP 响应主体为空。",
"error.http_forbidden": "该网站被禁止访问,网站可能有机器人保护机制?",
"error.http_gateway_timeout": "当前由于网关超时导致该网站无法访问,问题不在Miniflux,请稍后重试。",
"error.http_internal_server_error": "当前由于服务器错误导致该网站无法访问,问题不在Miniflux,请稍后重试。",
"error.http_gateway_timeout": "当前由于网关超时导致该网站无法访问,问题不在 Miniflux,请稍后重试。",
"error.http_internal_server_error": "当前由于服务器错误导致该网站无法访问,问题不在 Miniflux,请稍后重试。",
"error.http_not_authorized": "该网站访问未授权,可能用户名和密码错误。",
"error.http_resource_not_found": "请求资源无法找到,请检查URL。",
"error.http_response_too_large": "HTTP响应内容过大,您可以在全局设置中增加HTTP响应大小限制(需要服务器重新启动)。",
"error.http_service_unavailable": "当前由于服务器内部错误导致该网站无法访问,问题不在Miniflux,请稍后重试。",
"error.http_resource_not_found": "请求资源无法找到,请检查 URL。",
"error.http_response_too_large": "HTTP 响应内容过大,您可以在全局设置中增加 HTTP 响应大小限制(需要服务器重新启动)。",
"error.http_service_unavailable": "当前由于服务器内部错误导致该网站无法访问,问题不在 Miniflux,请稍后重试。",
"error.http_too_many_requests": "Miniflux 对该网站请求过多次数,请稍后重试或修改应用配置项。",
"error.http_unexpected_status_code": "当前由于意外的HTTP状态码:%d 导致该网站无法访问,问题不在Miniflux,请稍后重试。",
"error.http_unexpected_status_code": "当前由于意外的 HTTP 状态码:%d 导致该网站无法访问,问题不在 Miniflux,请稍后重试。",
"error.invalid_default_home_page": "无效的默认主页!",
"error.invalid_display_mode": "无效的网页应用显示模式。",
"error.invalid_entry_direction": "无效的输入方向。",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "订阅源的网址无效。",
"error.invalid_gesture_nav": "手势导航无效。",
"error.invalid_language": "无效的语言。",
"error.invalid_site_url": "源网站的网址无效。",
"error.invalid_theme": "无效的主题。",
"error.invalid_timezone": "无效的时区。",
"error.network_operation": "Miniflux无法访问该网站由于网络错误: %v。",
"error.network_operation": "Miniflux 无法访问该网站由于网络错误: %v。",
"error.network_timeout": "该网站响应过慢,请求超时: %v",
"error.password_min_length": "请至少输入 6 个字符",
"error.pocket_access_token": "无法从 Pocket 获取访问令牌!",
"error.pocket_request_token": "无法从 Pocket 获取请求令牌!",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"error.settings_block_rule_fieldname_invalid": "无效的阻止规则: 规则 #%d 缺少合法的字段名 (可选: %s)",
"error.settings_block_rule_invalid_regex": "无效的阻止规则: 规则 #%d 的模式字符不是合法的正则表达式。",
"error.settings_block_rule_regex_required": "无效的阻止规则: 规则 #%d 的模式字符没有提供。",
@@ -141,11 +143,11 @@
"error.site_url_not_empty": "源网站的网址不能为空。",
"error.subscription_not_found": "找不到任何源",
"error.title_required": "必须填写标题",
"error.tls_error": "TLS 错误: %q。如果您愿意的话可以在订阅源设置里关闭TLS验证。",
"error.tls_error": "TLS 错误: %q。如果您愿意的话可以在订阅源设置里关闭 TLS 验证。",
"error.unable_to_create_api_key": "无法创建此 API 密钥。",
"error.unable_to_create_category": "无法建立这个分类",
"error.unable_to_create_user": "无法创建此用户",
"error.unable_to_detect_rssbridge": "无法使用RSS-Bridge去检测订阅源: %v。",
"error.unable_to_detect_rssbridge": "无法使用 RSS-Bridge 去检测订阅源: %v。",
"error.unable_to_parse_feed": "无法解析该订阅源: %v。",
"error.unable_to_update_category": "无法更新该分类",
"error.unable_to_update_feed": "无法更新此源",
@@ -153,7 +155,7 @@
"error.unlink_account_without_password": "您必须设置密码,否则您将无法再次登录。",
"error.user_already_exists": "用户已存在",
"error.user_mandatory_fields": "必须填写用户名",
"form.api_key.label.description": "API密钥标签",
"form.api_key.label.description": "API 密钥标签",
"form.category.hide_globally": "隐藏全局未读列表中的文章",
"form.category.label.title": "标题",
"form.feed.fieldset.general": "通用",
@@ -172,40 +174,41 @@
"form.feed.label.feed_password": "源密码",
"form.feed.label.feed_url": "订阅源 URL",
"form.feed.label.feed_username": "源用户名",
"form.feed.label.fetch_via_proxy": "通过代理获取",
"form.feed.label.fetch_via_proxy": "使用在应用程序级别配置的代理",
"form.feed.label.hide_globally": "隐藏全局未读列表中的文章",
"form.feed.label.ignore_http_cache": "忽略 HTTP 缓存",
"form.feed.label.keeplist_rules": "保留规则",
"form.feed.label.no_media_player": "没有媒体播放器(音频/视频)",
"form.feed.label.ntfy_activate": "推送条目到ntfy",
"form.feed.label.ntfy_default_priority": "Ntfy默认优先级",
"form.feed.label.ntfy_high_priority": "Ntfy高优先级",
"form.feed.label.ntfy_low_priority": "Ntfy低优先级",
"form.feed.label.ntfy_max_priority": "Ntfy最高优先级",
"form.feed.label.ntfy_min_priority": "Ntfy最低优先级",
"form.feed.label.ntfy_priority": "Ntfy优先级",
"form.feed.label.ntfy_topic": "Ntfy主题(可选)",
"form.feed.label.pushover_activate": "Push entries to pushover.net",
"form.feed.label.pushover_default_priority": "Pushover default priority",
"form.feed.label.pushover_high_priority": "Pushover high priority",
"form.feed.label.pushover_low_priority": "Pushover low priority",
"form.feed.label.pushover_max_priority": "Pushover max priority",
"form.feed.label.pushover_min_priority": "Pushover min priority",
"form.feed.label.pushover_priority": "Pushover message priority",
"form.feed.label.ntfy_default_priority": "Ntfy 默认优先级",
"form.feed.label.ntfy_high_priority": "Ntfy 高优先级",
"form.feed.label.ntfy_low_priority": "Ntfy 低优先级",
"form.feed.label.ntfy_max_priority": "Ntfy 最高优先级",
"form.feed.label.ntfy_min_priority": "Ntfy 最低优先级",
"form.feed.label.ntfy_priority": "Ntfy 优先级",
"form.feed.label.ntfy_topic": "Ntfy 主题(可选)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "将条目推送至 pushover.net",
"form.feed.label.pushover_default_priority": "Pushover 默认优先级",
"form.feed.label.pushover_high_priority": "Pushover 高优先级",
"form.feed.label.pushover_low_priority": "Pushover 低优先级",
"form.feed.label.pushover_max_priority": "Pushover 最高优先级",
"form.feed.label.pushover_min_priority": "Pushover 最低优先级",
"form.feed.label.pushover_priority": "Pushover 消息优先级",
"form.feed.label.rewrite_rules": "重写规则",
"form.feed.label.scraper_rules": "抓取规则",
"form.feed.label.site_url": "源网站 URL",
"form.feed.label.title": "标题",
"form.feed.label.urlrewrite_rules": "URL 重写规则",
"form.feed.label.user_agent": "覆盖默认的用户代理",
"form.feed.label.webhook_url": "Override webhook url",
"form.feed.label.webhook_url": "覆盖 webhook url",
"form.import.label.file": "OPML 文件",
"form.import.label.url": "URL",
"form.integration.apprise_activate": "将新文章推送到 Apprise",
"form.integration.apprise_services_url": "使用逗号分隔的 Apprise 服务 URL 列表",
"form.integration.apprise_url": "Apprise API URL",
"form.integration.betula_activate": "保存文章到 Betula",
"form.integration.betula_token": "Betula 密钥",
"form.integration.betula_token": "Betula 令牌",
"form.integration.betula_url": "Betula 服务地址",
"form.integration.cubox_activate": "保存文章到 Cubox",
"form.integration.cubox_api_link": "Cubox API 链接",
@@ -247,15 +250,15 @@
"form.integration.matrix_bot_user": "Matrix Bot 用户名",
"form.integration.notion_activate": "保存文章到 Notion",
"form.integration.notion_page_id": "Notion 页面ID",
"form.integration.notion_token": "Notion 密钥",
"form.integration.ntfy_activate": "推送条目到ntfy",
"form.integration.notion_token": "Notion 令牌",
"form.integration.ntfy_activate": "推送条目到 ntfy",
"form.integration.ntfy_api_token": "Ntfy API令牌(可选)",
"form.integration.ntfy_icon_url": "Ntfy图标URL(可选)",
"form.integration.ntfy_internal_links": "Use internal links on click (optional)",
"form.integration.ntfy_password": "Ntfy密码(可选)",
"form.integration.ntfy_topic": "Ntfy主题(預設,如果未在此源中定義)",
"form.integration.ntfy_url": "Ntfy URL(可选,默认为ntfy.sh",
"form.integration.ntfy_username": "Ntfy用户名(可选)",
"form.integration.ntfy_icon_url": "Ntfy 图标 URL (可选)",
"form.integration.ntfy_internal_links": "点击时使用内部链接(可选)",
"form.integration.ntfy_password": "Ntfy 密码(可选)",
"form.integration.ntfy_topic": "Ntfy 主题(預設,如果未在此源中定義)",
"form.integration.ntfy_url": "Ntfy URL(可选,默认为 ntfy.sh",
"form.integration.ntfy_username": "Ntfy 用户名(可选)",
"form.integration.nunux_keeper_activate": "保存文章到 Nunux Keeper",
"form.integration.nunux_keeper_api_key": "Nunux Keeper API 密钥",
"form.integration.nunux_keeper_endpoint": "Nunux Keeper API 端点",
@@ -265,28 +268,28 @@
"form.integration.pinboard_activate": "保存文章到 Pinboard",
"form.integration.pinboard_bookmark": "标记为未读",
"form.integration.pinboard_tags": "Pinboard 标签",
"form.integration.pinboard_token": "Pinboard API 密钥",
"form.integration.pocket_access_token": "Pocket 访问密钥",
"form.integration.pinboard_token": "Pinboard API 令牌",
"form.integration.pocket_access_token": "Pocket 访问令牌",
"form.integration.pocket_activate": "将文章保存到 Pocket",
"form.integration.pocket_connect_link": "连接您的 Pocket 帐户",
"form.integration.pocket_consumer_key": "Pocket 用户密钥",
"form.integration.pushover_activate": "Push entries to Pushover",
"form.integration.pushover_device": "Pushover device (optional)",
"form.integration.pushover_prefix": "Pushover URL prefix (optional)",
"form.integration.pushover_token": "Pushover application API token",
"form.integration.pushover_user": "Pushover user key",
"form.integration.pushover_activate": "将条目推送至 Pushover",
"form.integration.pushover_device": "Pushover 装置(可选)",
"form.integration.pushover_prefix": "Pushover URL 前缀(可选)",
"form.integration.pushover_token": "Pushover 应用程序 API 令牌",
"form.integration.pushover_user": "Pushover 用户密钥",
"form.integration.raindrop_activate": "保存文章到 Raindrop",
"form.integration.raindrop_collection_id": "Collection ID",
"form.integration.raindrop_tags": "Tags (comma-separated)",
"form.integration.raindrop_token": "(Test) 密钥",
"form.integration.raindrop_collection_id": "集合 ID",
"form.integration.raindrop_tags": "Tags (逗号分隔)",
"form.integration.raindrop_token": "(Test) 令牌",
"form.integration.readeck_activate": "保存文章到 Readeck",
"form.integration.readeck_api_key": "Readeck API 密钥",
"form.integration.readeck_endpoint": "Readeck API 端点",
"form.integration.readeck_labels": "Readeck 默认标签",
"form.integration.readeck_only_url": "仅发送 URL(而不是完整内容)",
"form.integration.readwise_activate": "保存文章到 Readwise Reader",
"form.integration.readwise_api_key": "Readwise Reader 密钥",
"form.integration.readwise_api_key_link": "获取你的 Readwise 密钥",
"form.integration.readwise_api_key": "Readwise Reader 访问令牌",
"form.integration.readwise_api_key_link": "获取你的 Readwise 访问令牌",
"form.integration.rssbridge_activate": "添加订阅时检查 RSS-Bridge",
"form.integration.rssbridge_url": "RSS-Bridge 服务器 URL",
"form.integration.shaarli_activate": "保存文章到 Shaarli",
@@ -303,7 +306,7 @@
"form.integration.telegram_bot_disable_notification": "禁用通知",
"form.integration.telegram_bot_disable_web_page_preview": "禁用网页预览",
"form.integration.telegram_bot_token": "机器人令牌",
"form.integration.telegram_chat_id": "聊天ID",
"form.integration.telegram_chat_id": "聊天 ID",
"form.integration.telegram_topic_id": "Topic ID",
"form.integration.wallabag_activate": "保存文章到 Wallabag",
"form.integration.wallabag_client_id": "Wallabag 客户端 ID",
@@ -417,7 +420,7 @@
"page.api_keys.table.created_at": "创建日期",
"page.api_keys.table.description": "描述",
"page.api_keys.table.last_used_at": "最后使用",
"page.api_keys.table.token": "密钥",
"page.api_keys.table.token": "令牌",
"page.api_keys.title": "API 密钥",
"page.categories_count": [
"%d 分类"
+12 -9
View File
@@ -70,10 +70,10 @@
"entry.state.loading": "載入中…",
"entry.state.saving": "儲存中…",
"entry.status.mark_as_read": "標記為已讀",
"entry.status.mark_as_unread": "標記為未讀",
"entry.status.title": "更改狀態",
"entry.status.toast.read": "已標記為已讀",
"entry.status.toast.unread": "已標記為未讀",
"entry.status.mark_as_unread": "標記為未讀",
"entry.tags.label": "標籤:",
"entry.unshare.label": "取消分享",
"error.api_key_already_exists": "此 API 金鑰已存在。",
@@ -115,6 +115,7 @@
"error.invalid_default_home_page": "預設主頁無效!",
"error.invalid_display_mode": "無效的顯示模式。",
"error.invalid_entry_direction": "無效的輸入方向。",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "訂閱網址無效。",
"error.invalid_gesture_nav": "手勢導覽無效。",
"error.invalid_language": "無效的語言。",
@@ -126,6 +127,7 @@
"error.password_min_length": "請至少輸入 6 個字元",
"error.pocket_access_token": "無法從 Pocket 取得存取金鑰!",
"error.pocket_request_token": "無法從 Pocket 取得請求金鑰!",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"error.settings_block_rule_fieldname_invalid": "無效的封鎖規則:規則 #%d 缺少有效的欄位名稱 (可用選項:%s)",
"error.settings_block_rule_invalid_regex": "無效的封鎖規則:規則 #%d 的模式不是合法的正規表示式",
"error.settings_block_rule_regex_required": "無效的封鎖規則:規則 #%d 沒有提供正規表示式",
@@ -172,7 +174,7 @@
"form.feed.label.feed_password": "Feed 密碼",
"form.feed.label.feed_url": "Feed 網址",
"form.feed.label.feed_username": "Feed 使用者名稱",
"form.feed.label.fetch_via_proxy": "透過代理取得",
"form.feed.label.fetch_via_proxy": "使用應用程式層級設定的代理",
"form.feed.label.hide_globally": "在全域未讀列表中隱藏文章",
"form.feed.label.ignore_http_cache": "忽略 HTTP 快取",
"form.feed.label.keeplist_rules": "保留規則",
@@ -185,6 +187,7 @@
"form.feed.label.ntfy_min_priority": "Ntfy 最低優先順序",
"form.feed.label.ntfy_priority": "Ntfy 優先順序",
"form.feed.label.ntfy_topic": "Ntfy topic (選填)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "Push entries to pushover.net",
"form.feed.label.pushover_default_priority": "Pushover default priority",
"form.feed.label.pushover_high_priority": "Pushover high priority",
@@ -419,6 +422,9 @@
"page.api_keys.table.last_used_at": "最後使用",
"page.api_keys.table.token": "金鑰",
"page.api_keys.title": "API 金鑰",
"page.categories_count": [
"%d 個分類"
],
"page.categories.entries": "檢視內容",
"page.categories.feed_count": [
"有 %d 個 Feed"
@@ -426,9 +432,6 @@
"page.categories.feeds": "檢視 Feeds",
"page.categories.no_feed": "沒有 Feed",
"page.categories.title": "分類",
"page.categories_count": [
"%d 個分類"
],
"page.category_label": "分類:%s",
"page.edit_category.title": "編輯分類 : %s",
"page.edit_feed.etag_header": "ETag 標頭:",
@@ -531,21 +534,21 @@
"page.settings.webauthn.passkeys": "Passkeys",
"page.settings.webauthn.register": "註冊 Passkey",
"page.settings.webauthn.register.error": "無法註冊 Passkey",
"page.shared_entries.title": "已分享的文章",
"page.shared_entries_count": [
"已分享 %d 篇文章"
],
"page.starred.title": "收藏",
"page.shared_entries.title": "已分享的文章",
"page.starred_entry_count": [
"%d 篇收藏文章"
],
"page.starred.title": "收藏",
"page.total_entry_count": [
"總共 %d 篇文章"
],
"page.unread.title": "未讀",
"page.unread_entry_count": [
"%d 篇未讀文章"
],
"page.unread.title": "未讀",
"page.users.actions": "操作",
"page.users.admin.no": "否",
"page.users.admin.yes": "是",
@@ -586,4 +589,4 @@
"time_elapsed.yesterday": "昨天",
"tooltip.keyboard_shortcuts": "快捷鍵:%s",
"tooltip.logged_user": "目前登入 %s"
}
}
+15 -7
View File
@@ -19,16 +19,24 @@ func (c *Category) String() string {
return fmt.Sprintf("ID=%d, UserID=%d, Title=%s", c.ID, c.UserID, c.Title)
}
// CategoryRequest represents the request to create or update a category.
type CategoryRequest struct {
type CategoryCreationRequest struct {
Title string `json:"title"`
HideGlobally string `json:"hide_globally"`
HideGlobally bool `json:"hide_globally"`
}
// Patch updates category fields.
func (cr *CategoryRequest) Patch(category *Category) {
category.Title = cr.Title
category.HideGlobally = cr.HideGlobally != ""
type CategoryModificationRequest struct {
Title *string `json:"title"`
HideGlobally *bool `json:"hide_globally"`
}
func (c *CategoryModificationRequest) Patch(category *Category) {
if c.Title != nil {
category.Title = *c.Title
}
if c.HideGlobally != nil {
category.HideGlobally = *c.HideGlobally
}
}
// Categories represents a list of categories.
+20 -6
View File
@@ -57,8 +57,9 @@ type Feed struct {
NtfyEnabled bool `json:"ntfy_enabled"`
NtfyPriority int `json:"ntfy_priority"`
NtfyTopic string `json:"ntfy_topic"`
PushoverEnabled bool `json:"pushover_enabled,omitempty"`
PushoverPriority int `json:"pushover_priority,omitempty"`
PushoverEnabled bool `json:"pushover_enabled"`
PushoverPriority int `json:"pushover_priority"`
ProxyURL string `json:"proxy_url"`
// Non-persisted attributes
Category *Category `json:"category,omitempty"`
@@ -116,9 +117,7 @@ func (f *Feed) CheckedNow() {
}
// ScheduleNextCheck set "next_check_at" of a feed based on the scheduler selected from the configuration.
func (f *Feed) ScheduleNextCheck(weeklyCount int, refreshDelayInMinutes int) {
f.TTL = refreshDelayInMinutes
func (f *Feed) ScheduleNextCheck(weeklyCount int, refreshDelayInMinutes int) int {
// Default to the global config Polling Frequency.
intervalMinutes := config.Opts.SchedulerRoundRobinMinInterval()
@@ -132,12 +131,21 @@ func (f *Feed) ScheduleNextCheck(weeklyCount int, refreshDelayInMinutes int) {
}
}
// If the feed has a TTL or a Retry-After defined, we use it to make sure we don't check it too often.
// Use the RSS TTL field, Retry-After, Cache-Control or Expires HTTP headers if defined.
if refreshDelayInMinutes > 0 && refreshDelayInMinutes > intervalMinutes {
intervalMinutes = refreshDelayInMinutes
}
// Limit the max interval value for misconfigured feeds.
switch config.Opts.PollingScheduler() {
case SchedulerRoundRobin:
intervalMinutes = min(intervalMinutes, config.Opts.SchedulerRoundRobinMaxInterval())
case SchedulerEntryFrequency:
intervalMinutes = min(intervalMinutes, config.Opts.SchedulerEntryFrequencyMaxInterval())
}
f.NextCheckAt = time.Now().Add(time.Minute * time.Duration(intervalMinutes))
return intervalMinutes
}
// FeedCreationRequest represents the request to create a feed.
@@ -161,6 +169,7 @@ type FeedCreationRequest struct {
HideGlobally bool `json:"hide_globally"`
UrlRewriteRules string `json:"urlrewrite_rules"`
DisableHTTP2 bool `json:"disable_http2"`
ProxyURL string `json:"proxy_url"`
}
type FeedCreationRequestFromSubscriptionDiscovery struct {
@@ -195,6 +204,7 @@ type FeedModificationRequest struct {
FetchViaProxy *bool `json:"fetch_via_proxy"`
HideGlobally *bool `json:"hide_globally"`
DisableHTTP2 *bool `json:"disable_http2"`
ProxyURL *string `json:"proxy_url"`
}
// Patch updates a feed with modified values.
@@ -286,6 +296,10 @@ func (f *FeedModificationRequest) Patch(feed *Feed) {
if f.DisableHTTP2 != nil {
feed.DisableHTTP2 = *f.DisableHTTP2
}
if f.ProxyURL != nil {
feed.ProxyURL = *f.ProxyURL
}
}
// Feeds is a list of feed
+23
View File
@@ -144,6 +144,29 @@ func TestFeedScheduleNextCheckRoundRobinWithRefreshDelayBelowMinInterval(t *test
checkTargetInterval(t, feed, expectedInterval, timeBefore, "TestFeedScheduleNextCheckRoundRobinWithRefreshDelayBelowMinInterval")
}
func TestFeedScheduleNextCheckRoundRobinWithRefreshDelayAboveMaxInterval(t *testing.T) {
os.Clearenv()
var err error
parser := config.NewParser()
config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}
timeBefore := time.Now()
feed := &Feed{}
feed.ScheduleNextCheck(0, config.Opts.SchedulerRoundRobinMaxInterval()+30)
if feed.NextCheckAt.IsZero() {
t.Error(`The next_check_at must be set`)
}
expectedInterval := config.Opts.SchedulerRoundRobinMaxInterval()
checkTargetInterval(t, feed, expectedInterval, timeBefore, "TestFeedScheduleNextCheckRoundRobinWithRefreshDelayAboveMaxInterval")
}
func TestFeedScheduleNextCheckRoundRobinMinInterval(t *testing.T) {
minInterval := 1
os.Clearenv()
+4
View File
@@ -20,3 +20,7 @@ func OptionalString(value string) *string {
}
return nil
}
func SetOptionalField[T any](value T) *T {
return &value
}
+1
View File
@@ -10,6 +10,7 @@ type SubscriptionDiscoveryRequest struct {
Cookie string `json:"cookie"`
Username string `json:"username"`
Password string `json:"password"`
ProxyURL string `json:"proxy_url"`
FetchViaProxy bool `json:"fetch_via_proxy"`
AllowSelfSignedCertificates bool `json:"allow_self_signed_certificates"`
DisableHTTP2 bool `json:"disable_http2"`
+60
View File
@@ -0,0 +1,60 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package proxyrotator // import "miniflux.app/v2/internal/proxyrotator"
import (
"net/url"
"sync"
)
var ProxyRotatorInstance *ProxyRotator
// ProxyRotator manages a list of proxies and rotates through them.
type ProxyRotator struct {
proxies []*url.URL
currentIndex int
mutex sync.Mutex
}
// NewProxyRotator creates a new ProxyRotator with the given proxy URLs.
func NewProxyRotator(proxyURLs []string) (*ProxyRotator, error) {
parsedProxies := make([]*url.URL, 0, len(proxyURLs))
for _, p := range proxyURLs {
proxyURL, err := url.Parse(p)
if err != nil {
return nil, err
}
parsedProxies = append(parsedProxies, proxyURL)
}
return &ProxyRotator{
proxies: parsedProxies,
currentIndex: 0,
mutex: sync.Mutex{},
}, nil
}
// GetNextProxy returns the next proxy in the rotation.
func (pr *ProxyRotator) GetNextProxy() *url.URL {
pr.mutex.Lock()
defer pr.mutex.Unlock()
if len(pr.proxies) == 0 {
return nil
}
proxy := pr.proxies[pr.currentIndex]
pr.currentIndex = (pr.currentIndex + 1) % len(pr.proxies)
return proxy
}
// HasProxies checks if there are any proxies available in the rotator.
func (pr *ProxyRotator) HasProxies() bool {
pr.mutex.Lock()
defer pr.mutex.Unlock()
return len(pr.proxies) > 0
}
@@ -0,0 +1,71 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package proxyrotator // import "miniflux.app/v2/internal/proxyrotator"
import (
"testing"
)
func TestProxyRotator(t *testing.T) {
proxyURLs := []string{
"http://proxy1.example.com",
"http://proxy2.example.com",
"http://proxy3.example.com",
}
rotator, err := NewProxyRotator(proxyURLs)
if err != nil {
t.Fatalf("Failed to create ProxyRotator: %v", err)
}
if !rotator.HasProxies() {
t.Fatalf("Expected rotator to have proxies")
}
seenProxies := make(map[string]bool)
for range len(proxyURLs) * 2 {
proxy := rotator.GetNextProxy()
if proxy == nil {
t.Fatalf("Expected a proxy, got nil")
}
seenProxies[proxy.String()] = true
}
if len(seenProxies) != len(proxyURLs) {
t.Fatalf("Expected to see all proxies, but saw: %v", seenProxies)
}
}
func TestProxyRotatorEmpty(t *testing.T) {
rotator, err := NewProxyRotator([]string{})
if err != nil {
t.Fatalf("Failed to create ProxyRotator: %v", err)
}
if rotator.HasProxies() {
t.Fatalf("Expected rotator to have no proxies")
}
proxy := rotator.GetNextProxy()
if proxy != nil {
t.Fatalf("Expected no proxy, got: %v", proxy)
}
}
func TestProxyRotatorInvalidURL(t *testing.T) {
invalidProxyURLs := []string{
"http://validproxy.example.com",
"test|test://invalidproxy.example.com",
}
rotator, err := NewProxyRotator(invalidProxyURLs)
if err == nil {
t.Fatalf("Expected an error when creating ProxyRotator with invalid URLs, but got none")
}
if rotator != nil {
t.Fatalf("Expected rotator to be nil when initialization fails, but got: %v", rotator)
}
}
+38 -13
View File
@@ -6,11 +6,14 @@ package fetcher // import "miniflux.app/v2/internal/reader/fetcher"
import (
"crypto/tls"
"encoding/base64"
"fmt"
"log/slog"
"net"
"net/http"
"net/url"
"time"
"miniflux.app/v2/internal/proxyrotator"
)
const (
@@ -21,12 +24,14 @@ const (
type RequestBuilder struct {
headers http.Header
clientProxyURL string
clientProxyURL *url.URL
useClientProxy bool
clientTimeout int
withoutRedirects bool
ignoreTLSErrors bool
disableHTTP2 bool
proxyRotator *proxyrotator.ProxyRotator
feedProxyURL string
}
func NewRequestBuilder() *RequestBuilder {
@@ -78,16 +83,26 @@ func (r *RequestBuilder) WithUsernameAndPassword(username, password string) *Req
return r
}
func (r *RequestBuilder) WithProxy(proxyURL string) *RequestBuilder {
func (r *RequestBuilder) WithProxyRotator(proxyRotator *proxyrotator.ProxyRotator) *RequestBuilder {
r.proxyRotator = proxyRotator
return r
}
func (r *RequestBuilder) WithCustomApplicationProxyURL(proxyURL *url.URL) *RequestBuilder {
r.clientProxyURL = proxyURL
return r
}
func (r *RequestBuilder) UseProxy(value bool) *RequestBuilder {
func (r *RequestBuilder) UseCustomApplicationProxyURL(value bool) *RequestBuilder {
r.useClientProxy = value
return r
}
func (r *RequestBuilder) WithCustomFeedProxyURL(proxyURL string) *RequestBuilder {
r.feedProxyURL = proxyURL
return r
}
func (r *RequestBuilder) WithTimeout(timeout int) *RequestBuilder {
r.clientTimeout = timeout
return r
@@ -151,15 +166,25 @@ func (r *RequestBuilder) ExecuteRequest(requestURL string) (*http.Response, erro
transport.TLSNextProto = map[string]func(string, *tls.Conn) http.RoundTripper{}
}
if r.useClientProxy && r.clientProxyURL != "" {
if proxyURL, err := url.Parse(r.clientProxyURL); err != nil {
slog.Warn("Unable to parse proxy URL",
slog.String("proxy_url", r.clientProxyURL),
slog.Any("error", err),
)
} else {
transport.Proxy = http.ProxyURL(proxyURL)
var clientProxyURL *url.URL
switch {
case r.feedProxyURL != "":
var err error
clientProxyURL, err = url.Parse(r.feedProxyURL)
if err != nil {
return nil, fmt.Errorf(`fetcher: invalid feed proxy URL %q: %w`, r.feedProxyURL, err)
}
case r.useClientProxy && r.clientProxyURL != nil:
clientProxyURL = r.clientProxyURL
case r.proxyRotator != nil && r.proxyRotator.HasProxies():
clientProxyURL = r.proxyRotator.GetNextProxy()
}
var clientProxyURLRedacted string
if clientProxyURL != nil {
transport.Proxy = http.ProxyURL(clientProxyURL)
clientProxyURLRedacted = clientProxyURL.Redacted()
}
client := &http.Client{
@@ -189,8 +214,8 @@ func (r *RequestBuilder) ExecuteRequest(requestURL string) (*http.Response, erro
slog.String("url", req.URL.String()),
slog.Any("headers", req.Header),
slog.Bool("without_redirects", r.withoutRedirects),
slog.Bool("with_proxy", r.useClientProxy),
slog.String("proxy_url", r.clientProxyURL),
slog.Bool("use_app_client_proxy", r.useClientProxy),
slog.String("client_proxy_url", clientProxyURLRedacted),
slog.Bool("ignore_tls_errors", r.ignoreTLSErrors),
slog.Bool("disable_http2", r.disableHTTP2),
))
@@ -9,6 +9,7 @@ import (
"fmt"
"io"
"log/slog"
"math"
"net"
"net/http"
"net/url"
@@ -53,6 +54,33 @@ func (r *ResponseHandler) ETag() string {
return r.httpResponse.Header.Get("ETag")
}
func (r *ResponseHandler) ExpiresInMinutes() int {
expiresHeaderValue := r.httpResponse.Header.Get("Expires")
if expiresHeaderValue != "" {
t, err := time.Parse(time.RFC1123, expiresHeaderValue)
if err == nil {
return int(math.Ceil(time.Until(t).Minutes()))
}
}
return 0
}
func (r *ResponseHandler) CacheControlMaxAgeInMinutes() int {
cacheControlHeaderValue := r.httpResponse.Header.Get("Cache-Control")
if cacheControlHeaderValue != "" {
for _, directive := range strings.Split(cacheControlHeaderValue, ",") {
directive = strings.TrimSpace(directive)
if strings.HasPrefix(directive, "max-age=") {
maxAge, err := strconv.Atoi(strings.TrimPrefix(directive, "max-age="))
if err == nil {
return int(math.Ceil(float64(maxAge) / 60))
}
}
}
}
return 0
}
func (r *ResponseHandler) ParseRetryDelay() int {
retryAfterHeaderValue := r.httpResponse.Header.Get("Retry-After")
if retryAfterHeaderValue != "" {
@@ -102,3 +102,75 @@ func TestRetryDelay(t *testing.T) {
})
}
}
func TestExpiresInMinutes(t *testing.T) {
var testCases = map[string]struct {
ExpiresHeader string
ExpectedMinutes int
}{
"Empty header": {
ExpiresHeader: "",
ExpectedMinutes: 0,
},
"Valid Expires header": {
ExpiresHeader: time.Now().Add(10 * time.Minute).Format(time.RFC1123),
ExpectedMinutes: 10,
},
"Invalid Expires header": {
ExpiresHeader: "invalid-date",
ExpectedMinutes: 0,
},
}
for name, tc := range testCases {
t.Run(name, func(tt *testing.T) {
header := http.Header{}
header.Add("Expires", tc.ExpiresHeader)
rh := ResponseHandler{
httpResponse: &http.Response{
Header: header,
},
}
if tc.ExpectedMinutes != rh.ExpiresInMinutes() {
t.Errorf("Expected %d, got %d for scenario %q", tc.ExpectedMinutes, rh.ExpiresInMinutes(), name)
}
})
}
}
func TestCacheControlMaxAgeInMinutes(t *testing.T) {
var testCases = map[string]struct {
CacheControlHeader string
ExpectedMinutes int
}{
"Empty header": {
CacheControlHeader: "",
ExpectedMinutes: 0,
},
"Valid max-age": {
CacheControlHeader: "max-age=600",
ExpectedMinutes: 10,
},
"Invalid max-age": {
CacheControlHeader: "max-age=invalid",
ExpectedMinutes: 0,
},
"Multiple directives": {
CacheControlHeader: "no-cache, max-age=300",
ExpectedMinutes: 5,
},
}
for name, tc := range testCases {
t.Run(name, func(tt *testing.T) {
header := http.Header{}
header.Add("Cache-Control", tc.CacheControlHeader)
rh := ResponseHandler{
httpResponse: &http.Response{
Header: header,
},
}
if tc.ExpectedMinutes != rh.CacheControlMaxAgeInMinutes() {
t.Errorf("Expected %d, got %d for scenario %q", tc.ExpectedMinutes, rh.CacheControlMaxAgeInMinutes(), name)
}
})
}
}
+37 -11
View File
@@ -12,6 +12,7 @@ import (
"miniflux.app/v2/internal/integration"
"miniflux.app/v2/internal/locale"
"miniflux.app/v2/internal/model"
"miniflux.app/v2/internal/proxyrotator"
"miniflux.app/v2/internal/reader/fetcher"
"miniflux.app/v2/internal/reader/icon"
"miniflux.app/v2/internal/reader/parser"
@@ -29,6 +30,7 @@ func CreateFeedFromSubscriptionDiscovery(store *storage.Storage, userID int64, f
slog.Debug("Begin feed creation process from subscription discovery",
slog.Int64("user_id", userID),
slog.String("feed_url", feedCreationRequest.FeedURL),
slog.String("proxy_url", feedCreationRequest.ProxyURL),
)
if !store.CategoryIDExists(userID, feedCreationRequest.CategoryID) {
@@ -64,6 +66,7 @@ func CreateFeedFromSubscriptionDiscovery(store *storage.Storage, userID int64, f
subscription.FeedURL = feedCreationRequest.FeedURL
subscription.DisableHTTP2 = feedCreationRequest.DisableHTTP2
subscription.WithCategoryID(feedCreationRequest.CategoryID)
subscription.ProxyURL = feedCreationRequest.ProxyURL
subscription.CheckedNow()
processor.ProcessFeedEntries(store, subscription, userID, true)
@@ -83,8 +86,10 @@ func CreateFeedFromSubscriptionDiscovery(store *storage.Storage, userID int64, f
requestBuilder.WithUserAgent(feedCreationRequest.UserAgent, config.Opts.HTTPClientUserAgent())
requestBuilder.WithCookie(feedCreationRequest.Cookie)
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
requestBuilder.UseProxy(feedCreationRequest.FetchViaProxy)
requestBuilder.WithProxyRotator(proxyrotator.ProxyRotatorInstance)
requestBuilder.WithCustomFeedProxyURL(feedCreationRequest.ProxyURL)
requestBuilder.WithCustomApplicationProxyURL(config.Opts.HTTPClientProxyURL())
requestBuilder.UseCustomApplicationProxyURL(feedCreationRequest.FetchViaProxy)
requestBuilder.IgnoreTLSErrors(feedCreationRequest.AllowSelfSignedCertificates)
requestBuilder.DisableHTTP2(feedCreationRequest.DisableHTTP2)
@@ -98,6 +103,7 @@ func CreateFeed(store *storage.Storage, userID int64, feedCreationRequest *model
slog.Debug("Begin feed creation process",
slog.Int64("user_id", userID),
slog.String("feed_url", feedCreationRequest.FeedURL),
slog.String("proxy_url", feedCreationRequest.ProxyURL),
)
if !store.CategoryIDExists(userID, feedCreationRequest.CategoryID) {
@@ -109,8 +115,10 @@ func CreateFeed(store *storage.Storage, userID int64, feedCreationRequest *model
requestBuilder.WithUserAgent(feedCreationRequest.UserAgent, config.Opts.HTTPClientUserAgent())
requestBuilder.WithCookie(feedCreationRequest.Cookie)
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
requestBuilder.UseProxy(feedCreationRequest.FetchViaProxy)
requestBuilder.WithProxyRotator(proxyrotator.ProxyRotatorInstance)
requestBuilder.WithCustomFeedProxyURL(feedCreationRequest.ProxyURL)
requestBuilder.WithCustomApplicationProxyURL(config.Opts.HTTPClientProxyURL())
requestBuilder.UseCustomApplicationProxyURL(feedCreationRequest.FetchViaProxy)
requestBuilder.IgnoreTLSErrors(feedCreationRequest.AllowSelfSignedCertificates)
requestBuilder.DisableHTTP2(feedCreationRequest.DisableHTTP2)
@@ -157,6 +165,7 @@ func CreateFeed(store *storage.Storage, userID int64, feedCreationRequest *model
subscription.EtagHeader = responseHandler.ETag()
subscription.LastModifiedHeader = responseHandler.LastModified()
subscription.FeedURL = responseHandler.EffectiveURL()
subscription.ProxyURL = feedCreationRequest.ProxyURL
subscription.WithCategoryID(feedCreationRequest.CategoryID)
subscription.CheckedNow()
@@ -212,8 +221,10 @@ func RefreshFeed(store *storage.Storage, userID, feedID int64, forceRefresh bool
requestBuilder.WithUserAgent(originalFeed.UserAgent, config.Opts.HTTPClientUserAgent())
requestBuilder.WithCookie(originalFeed.Cookie)
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
requestBuilder.UseProxy(originalFeed.FetchViaProxy)
requestBuilder.WithProxyRotator(proxyrotator.ProxyRotatorInstance)
requestBuilder.WithCustomFeedProxyURL(originalFeed.ProxyURL)
requestBuilder.WithCustomApplicationProxyURL(config.Opts.HTTPClientProxyURL())
requestBuilder.UseCustomApplicationProxyURL(originalFeed.FetchViaProxy)
requestBuilder.IgnoreTLSErrors(originalFeed.AllowSelfSignedCertificates)
requestBuilder.DisableHTTP2(originalFeed.DisableHTTP2)
@@ -229,18 +240,24 @@ func RefreshFeed(store *storage.Storage, userID, feedID int64, forceRefresh bool
if responseHandler.IsRateLimited() {
retryDelayInSeconds := responseHandler.ParseRetryDelay()
refreshDelayInMinutes = retryDelayInSeconds / 60
originalFeed.ScheduleNextCheck(weeklyEntryCount, refreshDelayInMinutes)
calculatedNextCheckIntervalInMinutes := originalFeed.ScheduleNextCheck(weeklyEntryCount, refreshDelayInMinutes)
slog.Warn("Feed is rate limited",
slog.String("feed_url", originalFeed.FeedURL),
slog.Int("retry_delay_in_seconds", retryDelayInSeconds),
slog.Int("refresh_delay_in_minutes", refreshDelayInMinutes),
slog.Int("calculated_next_check_interval_in_minutes", calculatedNextCheckIntervalInMinutes),
slog.Time("new_next_check_at", originalFeed.NextCheckAt),
)
}
if localizedError := responseHandler.LocalizedError(); localizedError != nil {
slog.Warn("Unable to fetch feed", slog.String("feed_url", originalFeed.FeedURL), slog.Any("error", localizedError.Error()))
slog.Warn("Unable to fetch feed",
slog.Int64("user_id", userID),
slog.Int64("feed_id", feedID),
slog.String("feed_url", originalFeed.FeedURL),
slog.Any("error", localizedError.Error()),
)
user, storeErr := store.UserByID(userID)
if storeErr != nil {
return locale.NewLocalizedErrorWrapper(storeErr, "error.database_error", storeErr)
@@ -292,16 +309,25 @@ func RefreshFeed(store *storage.Storage, userID, feedID int64, forceRefresh bool
return localizedError
}
// If the feed has a TTL defined, we use it to make sure we don't check it too often.
refreshDelayInMinutes = updatedFeed.TTL
// Use the RSS TTL value, or the Cache-Control or Expires HTTP headers if available.
// Otherwise, we use the default value from the configuration (min interval parameter).
feedTTLValue := updatedFeed.TTL
cacheControlMaxAgeValue := responseHandler.CacheControlMaxAgeInMinutes()
expiresValue := responseHandler.ExpiresInMinutes()
refreshDelayInMinutes = max(feedTTLValue, cacheControlMaxAgeValue, expiresValue)
// Set the next check at with updated arguments.
originalFeed.ScheduleNextCheck(weeklyEntryCount, refreshDelayInMinutes)
calculatedNextCheckIntervalInMinutes := originalFeed.ScheduleNextCheck(weeklyEntryCount, refreshDelayInMinutes)
slog.Debug("Updated next check date",
slog.Int64("user_id", userID),
slog.Int64("feed_id", feedID),
slog.String("feed_url", originalFeed.FeedURL),
slog.Int("feed_ttl_minutes", feedTTLValue),
slog.Int("cache_control_max_age_in_minutes", cacheControlMaxAgeValue),
slog.Int("expires_in_minutes", expiresValue),
slog.Int("refresh_delay_in_minutes", refreshDelayInMinutes),
slog.Int("calculated_next_check_interval_in_minutes", calculatedNextCheckIntervalInMinutes),
slog.Time("new_next_check_at", originalFeed.NextCheckAt),
)
+5 -2
View File
@@ -8,6 +8,7 @@ import (
"miniflux.app/v2/internal/config"
"miniflux.app/v2/internal/model"
"miniflux.app/v2/internal/proxyrotator"
"miniflux.app/v2/internal/reader/fetcher"
"miniflux.app/v2/internal/storage"
)
@@ -29,8 +30,10 @@ func (c *IconChecker) fetchAndStoreIcon() {
requestBuilder.WithUserAgent(c.feed.UserAgent, config.Opts.HTTPClientUserAgent())
requestBuilder.WithCookie(c.feed.Cookie)
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
requestBuilder.UseProxy(c.feed.FetchViaProxy)
requestBuilder.WithProxyRotator(proxyrotator.ProxyRotatorInstance)
requestBuilder.WithCustomFeedProxyURL(c.feed.ProxyURL)
requestBuilder.WithCustomApplicationProxyURL(config.Opts.HTTPClientProxyURL())
requestBuilder.UseCustomApplicationProxyURL(c.feed.FetchViaProxy)
requestBuilder.IgnoreTLSErrors(c.feed.AllowSelfSignedCertificates)
requestBuilder.DisableHTTP2(c.feed.DisableHTTP2)
+1 -1
View File
@@ -61,7 +61,7 @@ func (h *Handler) Import(userID int64, data io.Reader) error {
}
if category == nil {
category, err = h.store.CreateCategory(userID, &model.CategoryRequest{Title: subscription.CategoryName})
category, err = h.store.CreateCategory(userID, &model.CategoryCreationRequest{Title: subscription.CategoryName})
if err != nil {
return fmt.Errorf(`opml: unable to create this category: %q`, subscription.CategoryName)
}
+2 -1
View File
@@ -11,6 +11,7 @@ import (
"miniflux.app/v2/internal/config"
"miniflux.app/v2/internal/model"
"miniflux.app/v2/internal/proxyrotator"
"miniflux.app/v2/internal/reader/fetcher"
)
@@ -45,7 +46,7 @@ func extractBilibiliVideoID(websiteURL string) (string, string, error) {
func fetchBilibiliWatchTime(websiteURL string) (int, error) {
requestBuilder := fetcher.NewRequestBuilder()
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
requestBuilder.WithProxyRotator(proxyrotator.ProxyRotatorInstance)
idType, videoID, extractErr := extractBilibiliVideoID(websiteURL)
if extractErr != nil {
+2 -1
View File
@@ -14,6 +14,7 @@ import (
"miniflux.app/v2/internal/config"
"miniflux.app/v2/internal/model"
"miniflux.app/v2/internal/proxyrotator"
"miniflux.app/v2/internal/reader/fetcher"
)
@@ -33,7 +34,7 @@ func shouldFetchNebulaWatchTime(entry *model.Entry) bool {
func fetchNebulaWatchTime(websiteURL string) (int, error) {
requestBuilder := fetcher.NewRequestBuilder()
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
requestBuilder.WithProxyRotator(proxyrotator.ProxyRotatorInstance)
responseHandler := fetcher.NewResponseHandler(requestBuilder.ExecuteRequest(websiteURL))
defer responseHandler.Close()
+2 -1
View File
@@ -14,6 +14,7 @@ import (
"miniflux.app/v2/internal/config"
"miniflux.app/v2/internal/model"
"miniflux.app/v2/internal/proxyrotator"
"miniflux.app/v2/internal/reader/fetcher"
)
@@ -33,7 +34,7 @@ func shouldFetchOdyseeWatchTime(entry *model.Entry) bool {
func fetchOdyseeWatchTime(websiteURL string) (int, error) {
requestBuilder := fetcher.NewRequestBuilder()
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
requestBuilder.WithProxyRotator(proxyrotator.ProxyRotatorInstance)
responseHandler := fetcher.NewResponseHandler(requestBuilder.ExecuteRequest(websiteURL))
defer responseHandler.Close()
+9 -4
View File
@@ -14,6 +14,7 @@ import (
"miniflux.app/v2/internal/config"
"miniflux.app/v2/internal/metric"
"miniflux.app/v2/internal/model"
"miniflux.app/v2/internal/proxyrotator"
"miniflux.app/v2/internal/reader/fetcher"
"miniflux.app/v2/internal/reader/readingtime"
"miniflux.app/v2/internal/reader/rewrite"
@@ -78,8 +79,10 @@ func ProcessFeedEntries(store *storage.Storage, feed *model.Feed, userID int64,
requestBuilder.WithUserAgent(feed.UserAgent, config.Opts.HTTPClientUserAgent())
requestBuilder.WithCookie(feed.Cookie)
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
requestBuilder.UseProxy(feed.FetchViaProxy)
requestBuilder.WithProxyRotator(proxyrotator.ProxyRotatorInstance)
requestBuilder.WithCustomFeedProxyURL(feed.ProxyURL)
requestBuilder.WithCustomApplicationProxyURL(config.Opts.HTTPClientProxyURL())
requestBuilder.UseCustomApplicationProxyURL(feed.FetchViaProxy)
requestBuilder.IgnoreTLSErrors(feed.AllowSelfSignedCertificates)
requestBuilder.DisableHTTP2(feed.DisableHTTP2)
@@ -145,8 +148,10 @@ func ProcessEntryWebPage(feed *model.Feed, entry *model.Entry, user *model.User)
requestBuilder.WithUserAgent(feed.UserAgent, config.Opts.HTTPClientUserAgent())
requestBuilder.WithCookie(feed.Cookie)
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
requestBuilder.UseProxy(feed.FetchViaProxy)
requestBuilder.WithProxyRotator(proxyrotator.ProxyRotatorInstance)
requestBuilder.WithCustomFeedProxyURL(feed.ProxyURL)
requestBuilder.WithCustomApplicationProxyURL(config.Opts.HTTPClientProxyURL())
requestBuilder.UseCustomApplicationProxyURL(feed.FetchViaProxy)
requestBuilder.IgnoreTLSErrors(feed.AllowSelfSignedCertificates)
requestBuilder.DisableHTTP2(feed.DisableHTTP2)
+3 -2
View File
@@ -18,6 +18,7 @@ import (
"miniflux.app/v2/internal/config"
"miniflux.app/v2/internal/model"
"miniflux.app/v2/internal/proxyrotator"
"miniflux.app/v2/internal/reader/fetcher"
)
@@ -52,7 +53,7 @@ func fetchYouTubeWatchTimeForSingleEntry(websiteURL string) (int, error) {
requestBuilder := fetcher.NewRequestBuilder()
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
requestBuilder.WithProxyRotator(proxyrotator.ProxyRotatorInstance)
responseHandler := fetcher.NewResponseHandler(requestBuilder.ExecuteRequest(websiteURL))
defer responseHandler.Close()
@@ -132,7 +133,7 @@ func fetchYouTubeWatchTimeFromApiInBulk(videoIDs []string) (map[string]time.Dura
requestBuilder := fetcher.NewRequestBuilder()
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
requestBuilder.WithProxyRotator(proxyrotator.ProxyRotatorInstance)
responseHandler := fetcher.NewResponseHandler(requestBuilder.ExecuteRequest(apiURL.String()))
defer responseHandler.Close()
+8 -5
View File
@@ -165,27 +165,30 @@ func (s *Storage) CategoriesWithFeedCount(userID int64) (model.Categories, error
}
// CreateCategory creates a new category.
func (s *Storage) CreateCategory(userID int64, request *model.CategoryRequest) (*model.Category, error) {
func (s *Storage) CreateCategory(userID int64, request *model.CategoryCreationRequest) (*model.Category, error) {
var category model.Category
query := `
INSERT INTO categories
(user_id, title)
(user_id, title, hide_globally)
VALUES
($1, $2)
($1, $2, $3)
RETURNING
id,
user_id,
title
title,
hide_globally
`
err := s.db.QueryRow(
query,
userID,
request.Title,
request.HideGlobally,
).Scan(
&category.ID,
&category.UserID,
&category.Title,
&category.HideGlobally,
)
if err != nil {
@@ -197,7 +200,7 @@ func (s *Storage) CreateCategory(userID int64, request *model.CategoryRequest) (
// UpdateCategory updates an existing category.
func (s *Storage) UpdateCategory(category *model.Category) error {
query := `UPDATE categories SET title=$1, hide_globally = $2 WHERE id=$3 AND user_id=$4`
query := `UPDATE categories SET title=$1, hide_globally=$2 WHERE id=$3 AND user_id=$4`
_, err := s.db.Exec(
query,
category.Title,
+13 -4
View File
@@ -248,10 +248,11 @@ func (s *Storage) CreateFeed(feed *model.Feed) error {
apprise_service_urls,
webhook_url,
disable_http2,
description
description,
proxy_url
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27)
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28)
RETURNING
id
`
@@ -284,6 +285,7 @@ func (s *Storage) CreateFeed(feed *model.Feed) error {
feed.WebhookURL,
feed.DisableHTTP2,
feed.Description,
feed.ProxyURL,
).Scan(&feed.ID)
if err != nil {
return fmt.Errorf(`store: unable to create feed %q: %v`, feed.FeedURL, err)
@@ -363,9 +365,10 @@ func (s *Storage) UpdateFeed(feed *model.Feed) (err error) {
ntfy_priority=$32,
ntfy_topic=$33,
pushover_enabled=$34,
pushover_priority=$35
pushover_priority=$35,
proxy_url=$36
WHERE
id=$36 AND user_id=$37
id=$37 AND user_id=$38
`
_, err = s.db.Exec(query,
feed.FeedURL,
@@ -403,6 +406,7 @@ func (s *Storage) UpdateFeed(feed *model.Feed) (err error) {
feed.NtfyTopic,
feed.PushoverEnabled,
feed.PushoverPriority,
feed.ProxyURL,
feed.ID,
feed.UserID,
)
@@ -481,3 +485,8 @@ func (s *Storage) ResetFeedErrors() error {
_, err := s.db.Exec(`UPDATE feeds SET parsing_error_count=0, parsing_error_msg=''`)
return err
}
func (s *Storage) ResetNextCheckAt() error {
_, err := s.db.Exec(`UPDATE feeds SET next_check_at=now()`)
return err
}
+3 -1
View File
@@ -172,7 +172,8 @@ func (f *FeedQueryBuilder) GetFeeds() (model.Feeds, error) {
f.ntfy_priority,
f.ntfy_topic,
f.pushover_enabled,
f.pushover_priority
f.pushover_priority,
f.proxy_url
FROM
feeds f
LEFT JOIN
@@ -251,6 +252,7 @@ func (f *FeedQueryBuilder) GetFeeds() (model.Feeds, error) {
&feed.NtfyTopic,
&feed.PushoverEnabled,
&feed.PushoverPriority,
&feed.ProxyURL,
)
if err != nil {
@@ -39,6 +39,9 @@
<label><input type="checkbox" name="fetch_via_proxy" value="1" {{ if .form.FetchViaProxy }}checked{{ end }}> {{ t "form.feed.label.fetch_via_proxy" }}</label>
{{ end }}
<label for="form-proxy-url">{{ t "form.feed.label.proxy_url" }}</label>
<input type="url" name="proxy_url" id="form-proxy-url" value="{{ .form.ProxyURL }}" spellcheck="false">
<label for="form-user-agent">{{ t "form.feed.label.user_agent" }}</label>
<input type="text" name="user_agent" id="form-user-agent" placeholder="{{ .defaultUserAgent }}" value="{{ .form.UserAgent }}" spellcheck="false" autocomplete="off">
@@ -20,6 +20,7 @@
<input type="hidden" name="blocklist_rules" value="{{ .form.BlocklistRules }}">
<input type="hidden" name="keeplist_rules" value="{{ .form.KeeplistRules }}">
<input type="hidden" name="urlrewrite_rules" value="{{ .form.UrlRewriteRules }}">
<input type="hidden" name="proxy_url" value="{{ .form.ProxyURL }}">
{{ if .form.FetchViaProxy }}
<input type="hidden" name="fetch_via_proxy" value="1">
{{ end }}
@@ -31,7 +31,7 @@
<input type="text" name="title" id="form-title" value="{{ .form.Title }}" required autofocus>
<label>
<input type="checkbox" name="hide_globally" {{ if .form.HideGlobally }}checked{{ end }}>
<input type="checkbox" name="hide_globally" {{ if .form.HideGlobally }}checked{{ end }} value="1">
{{ t "form.category.hide_globally" }}
</label>
@@ -97,6 +97,9 @@
<label for="form-user-agent">{{ t "form.feed.label.user_agent" }}</label>
<input type="text" name="user_agent" id="form-user-agent" placeholder="{{ .defaultUserAgent }}" value="{{ .form.UserAgent }}" spellcheck="false">
<label for="form-proxy-url">{{ t "form.feed.label.proxy_url" }}</label>
<input type="url" name="proxy_url" id="form-proxy-url" value="{{ .form.ProxyURL }}" spellcheck="false">
<label for="form-cookie">{{ t "form.feed.label.cookie" }}</label>
<input type="text" name="cookie" id="form-cookie" value="{{ .form.Cookie }}" spellcheck="false">
+6 -5
View File
@@ -67,11 +67,12 @@
</li>
{{ else }}
<li>
<a href="{{ route "shareEntry" "entryID" .entry.ID }}"
class="page-link"
title="{{ t "entry.share.title" }}"
data-share-status="share"
target="_blank">{{ icon "share" }}<span class="icon-label">{{ t "entry.share.label" }}</span></a>
<form method="post" action="{{route "shareEntry" "entryID" .entry.ID }}">
<input type="hidden" name="csrf" value="{{ .csrf }}">
<button type="submit" class="page-button">
{{ icon "share" }}<span class="icon-label">{{ t "entry.share.label" }}</span>
</button>
</form>
</li>
{{ end }}
<li>
+2 -6
View File
@@ -20,8 +20,7 @@ func (h *handler) showEditCategoryPage(w http.ResponseWriter, r *http.Request) {
return
}
categoryID := request.RouteInt64Param(r, "categoryID")
category, err := h.store.Category(request.UserID(r), categoryID)
category, err := h.store.Category(request.UserID(r), request.RouteInt64Param(r, "categoryID"))
if err != nil {
html.ServerError(w, r, err)
return
@@ -34,10 +33,7 @@ func (h *handler) showEditCategoryPage(w http.ResponseWriter, r *http.Request) {
categoryForm := form.CategoryForm{
Title: category.Title,
HideGlobally: "",
}
if category.HideGlobally {
categoryForm.HideGlobally = "checked"
HideGlobally: category.HideGlobally,
}
sess := session.New(h.store, request.SessionID(r))
+3 -3
View File
@@ -33,15 +33,15 @@ func (h *handler) saveCategory(w http.ResponseWriter, r *http.Request) {
view.Set("countUnread", h.store.CountUnreadEntries(loggedUser.ID))
view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(loggedUser.ID))
categoryRequest := &model.CategoryRequest{Title: categoryForm.Title}
categoryCreationRequest := &model.CategoryCreationRequest{Title: categoryForm.Title}
if validationErr := validator.ValidateCategoryCreation(h.store, loggedUser.ID, categoryRequest); validationErr != nil {
if validationErr := validator.ValidateCategoryCreation(h.store, loggedUser.ID, categoryCreationRequest); validationErr != nil {
view.Set("errorMessage", validationErr.Translate(loggedUser.Language))
html.OK(w, r, view.Render("create_category"))
return
}
if _, err = h.store.CreateCategory(loggedUser.ID, categoryRequest); err != nil {
if _, err = h.store.CreateCategory(loggedUser.ID, categoryCreationRequest); err != nil {
html.ServerError(w, r, err)
return
}
+3 -3
View File
@@ -46,9 +46,9 @@ func (h *handler) updateCategory(w http.ResponseWriter, r *http.Request) {
view.Set("countUnread", h.store.CountUnreadEntries(loggedUser.ID))
view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(loggedUser.ID))
categoryRequest := &model.CategoryRequest{
Title: categoryForm.Title,
HideGlobally: categoryForm.HideGlobally,
categoryRequest := &model.CategoryModificationRequest{
Title: model.SetOptionalField(categoryForm.Title),
HideGlobally: model.SetOptionalField(categoryForm.HideGlobally),
}
if validationErr := validator.ValidateCategoryModification(h.store, loggedUser.ID, category.ID, categoryRequest); validationErr != nil {
+2 -1
View File
@@ -70,6 +70,7 @@ func (h *handler) showEditFeedPage(w http.ResponseWriter, r *http.Request) {
NtfyTopic: feed.NtfyTopic,
PushoverEnabled: feed.PushoverEnabled,
PushoverPriority: feed.PushoverPriority,
ProxyURL: feed.ProxyURL,
}
sess := session.New(h.store, request.SessionID(r))
@@ -82,7 +83,7 @@ func (h *handler) showEditFeedPage(w http.ResponseWriter, r *http.Request) {
view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
view.Set("defaultUserAgent", config.Opts.HTTPClientUserAgent())
view.Set("hasProxyConfigured", config.Opts.HasHTTPClientProxyConfigured())
view.Set("hasProxyConfigured", config.Opts.HasHTTPClientProxyURLConfigured())
html.OK(w, r, view.Render("edit_feed"))
}
+1
View File
@@ -64,6 +64,7 @@ func (h *handler) updateFeed(w http.ResponseWriter, r *http.Request) {
BlocklistRules: model.OptionalString(feedForm.BlocklistRules),
KeeplistRules: model.OptionalString(feedForm.KeeplistRules),
UrlRewriteRules: model.OptionalString(feedForm.UrlRewriteRules),
ProxyURL: model.OptionalString(feedForm.ProxyURL),
}
if validationErr := validator.ValidateFeedModification(h.store, loggedUser.ID, feed.ID, feedModificationRequest); validationErr != nil {
+2 -2
View File
@@ -10,13 +10,13 @@ import (
// CategoryForm represents a feed form in the UI
type CategoryForm struct {
Title string
HideGlobally string
HideGlobally bool
}
// NewCategoryForm returns a new CategoryForm.
func NewCategoryForm(r *http.Request) *CategoryForm {
return &CategoryForm{
Title: r.FormValue("title"),
HideGlobally: r.FormValue("hide_globally"),
HideGlobally: r.FormValue("hide_globally") == "1",
}
}
+4
View File
@@ -42,6 +42,7 @@ type FeedForm struct {
NtfyTopic string
PushoverEnabled bool
PushoverPriority int
ProxyURL string
}
// Merge updates the fields of the given feed.
@@ -77,6 +78,7 @@ func (f FeedForm) Merge(feed *model.Feed) *model.Feed {
feed.NtfyTopic = f.NtfyTopic
feed.PushoverEnabled = f.PushoverEnabled
feed.PushoverPriority = f.PushoverPriority
feed.ProxyURL = f.ProxyURL
return feed
}
@@ -86,6 +88,7 @@ func NewFeedForm(r *http.Request) *FeedForm {
if err != nil {
categoryID = 0
}
ntfyPriority, err := strconv.Atoi(r.FormValue("ntfy_priority"))
if err != nil {
ntfyPriority = 0
@@ -126,5 +129,6 @@ func NewFeedForm(r *http.Request) *FeedForm {
NtfyTopic: r.FormValue("ntfy_topic"),
PushoverEnabled: r.FormValue("pushover_enabled") == "1",
PushoverPriority: pushoverPriority,
ProxyURL: r.FormValue("proxy_url"),
}
}
+6
View File
@@ -28,6 +28,7 @@ type SubscriptionForm struct {
KeeplistRules string
UrlRewriteRules string
DisableHTTP2 bool
ProxyURL string
}
// Validate makes sure the form values locale.are valid.
@@ -52,6 +53,10 @@ func (s *SubscriptionForm) Validate() *locale.LocalizedError {
return locale.NewLocalizedError("error.feed_invalid_urlrewrite_rule")
}
if s.ProxyURL != "" && !validator.IsValidURL(s.ProxyURL) {
return locale.NewLocalizedError("error.invalid_feed_proxy_url")
}
return nil
}
@@ -78,5 +83,6 @@ func NewSubscriptionForm(r *http.Request) *SubscriptionForm {
KeeplistRules: r.FormValue("keeplist_rules"),
UrlRewriteRules: r.FormValue("urlrewrite_rules"),
DisableHTTP2: r.FormValue("disable_http2") == "1",
ProxyURL: r.FormValue("proxy_url"),
}
}
+2 -1
View File
@@ -13,6 +13,7 @@ import (
"miniflux.app/v2/internal/http/response/html"
"miniflux.app/v2/internal/http/route"
"miniflux.app/v2/internal/locale"
"miniflux.app/v2/internal/proxyrotator"
"miniflux.app/v2/internal/reader/fetcher"
"miniflux.app/v2/internal/reader/opml"
"miniflux.app/v2/internal/ui/session"
@@ -95,7 +96,7 @@ func (h *handler) fetchOPML(w http.ResponseWriter, r *http.Request) {
requestBuilder := fetcher.NewRequestBuilder()
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
requestBuilder.WithProxyRotator(proxyrotator.ProxyRotatorInstance)
responseHandler := fetcher.NewResponseHandler(requestBuilder.ExecuteRequest(opmlFileURL))
defer responseHandler.Close()
+53 -12
View File
@@ -1,3 +1,44 @@
/**
* Open a new tab with the given URL.
*
* @param {string} url
*/
function openNewTab(url) {
const win = window.open("");
win.opener = null;
win.location = url;
win.focus();
}
/**
* Filter visible elements based on the selector.
*
* @param {string} selector
* @returns {Array<Element>}
*/
function getVisibleElements(selector) {
const elements = document.querySelectorAll(selector);
return [...elements].filter((element) => element.offsetParent !== null);
}
/**
* Scroll the page to the given element.
*
* @param {Element} element
* @param {boolean} evenIfOnScreen
*/
function scrollPageTo(element, evenIfOnScreen) {
const windowScrollPosition = window.scrollY;
const windowHeight = document.documentElement.clientHeight;
const viewportPosition = windowScrollPosition + windowHeight;
const itemBottomPosition = element.offsetTop + element.offsetHeight;
if (evenIfOnScreen || viewportPosition - itemBottomPosition < 0 || viewportPosition - element.offsetTop > windowHeight) {
window.scrollTo(0, element.offsetTop - 10);
}
}
// OnClick attaches a listener to the elements that match the selector.
function onClick(selector, callback, noPreventDefault) {
document.querySelectorAll(selector).forEach((element) => {
@@ -113,7 +154,7 @@ function showKeyboardShortcuts() {
// Mark as read visible items of the current page.
function markPageAsRead() {
const items = DomHelper.getVisibleElements(".items .item");
const items = getVisibleElements(".items .item");
const entryIDs = [];
items.forEach((element) => {
@@ -369,14 +410,14 @@ function openOriginalLink(openLinkInCurrentTab) {
if (openLinkInCurrentTab) {
window.location.href = entryLink.getAttribute("href");
} else {
DomHelper.openNewTab(entryLink.getAttribute("href"));
openNewTab(entryLink.getAttribute("href"));
}
return;
}
const currentItemOriginalLink = document.querySelector(".current-item :is(a, button)[data-original-link]");
if (currentItemOriginalLink !== null) {
DomHelper.openNewTab(currentItemOriginalLink.getAttribute("href"));
openNewTab(currentItemOriginalLink.getAttribute("href"));
const currentItem = document.querySelector(".current-item");
// If we are not on the list of starred items, move to the next item
@@ -394,13 +435,13 @@ function openCommentLink(openLinkInCurrentTab) {
if (openLinkInCurrentTab) {
window.location.href = entryLink.getAttribute("href");
} else {
DomHelper.openNewTab(entryLink.getAttribute("href"));
openNewTab(entryLink.getAttribute("href"));
}
}
} else {
const currentItemCommentsLink = document.querySelector(".current-item :is(a, button)[data-comments-link]");
if (currentItemCommentsLink !== null) {
DomHelper.openNewTab(currentItemCommentsLink.getAttribute("href"));
openNewTab(currentItemCommentsLink.getAttribute("href"));
}
}
}
@@ -503,7 +544,7 @@ const BOTTOM = -9999;
* @param {number} offset How many items to jump for focus.
*/
function goToListItem(offset) {
const items = DomHelper.getVisibleElements(".items .item");
const items = getVisibleElements(".items .item");
if (items.length === 0) {
return;
}
@@ -529,7 +570,7 @@ function goToListItem(offset) {
const item = items[itemOffset];
item.classList.add("current-item");
DomHelper.scrollPageTo(item);
scrollPageTo(item);
item.focus();
break;
@@ -540,7 +581,7 @@ function goToListItem(offset) {
function scrollToCurrentItem() {
const currentItem = document.querySelector(".current-item");
if (currentItem !== null) {
DomHelper.scrollPageTo(currentItem, true);
scrollPageTo(currentItem, true);
}
}
@@ -719,11 +760,11 @@ function isPlayerPlaying(element) {
/**
* handle new share entires and already shared entries
*/
function handleShare() {
async function handleShare() {
const link = document.querySelector(':is(a, button)[data-share-status]');
const title = document.querySelector(".entry-header > h1 > a");
if (link.dataset.shareStatus === "shared") {
checkShareAPI(title, link.href);
await checkShareAPI(title, link.href);
}
if (link.dataset.shareStatus === "share") {
const request = new RequestBuilder(link.href);
@@ -738,14 +779,14 @@ function handleShare() {
/**
* wrapper for Web Share API
*/
function checkShareAPI(title, url) {
async function checkShareAPI(title, url) {
if (!navigator.canShare) {
console.error("Your browser doesn't support the Web Share API.");
window.location = url;
return;
}
try {
navigator.share({
await navigator.share({
title: title ? title.textContent : url,
url: url
});
-28
View File
@@ -1,28 +0,0 @@
class DomHelper {
static isVisible(element) {
return element.offsetParent !== null;
}
static openNewTab(url) {
const win = window.open("");
win.opener = null;
win.location = url;
win.focus();
}
static scrollPageTo(element, evenIfOnScreen) {
const windowScrollPosition = window.scrollY;
const windowHeight = document.documentElement.clientHeight;
const viewportPosition = windowScrollPosition + windowHeight;
const itemBottomPosition = element.offsetTop + element.offsetHeight;
if (evenIfOnScreen || viewportPosition - itemBottomPosition < 0 || viewportPosition - element.offsetTop > windowHeight) {
window.scrollTo(0, element.offsetTop - 10);
}
}
static getVisibleElements(selector) {
const elements = document.querySelectorAll(selector);
return [...elements].filter((element) => this.isVisible(element));
}
}
-1
View File
@@ -114,7 +114,6 @@ func GenerateJavascriptBundles() error {
var bundles = map[string][]string{
"app": {
"js/tt.js", // has to be first
"js/dom_helper.js",
"js/touch_handler.js",
"js/keyboard_handler.js",
"js/request_builder.js",
+1 -1
View File
@@ -36,7 +36,7 @@ func (h *handler) showAddSubscriptionPage(w http.ResponseWriter, r *http.Request
view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
view.Set("defaultUserAgent", config.Opts.HTTPClientUserAgent())
view.Set("form", &form.SubscriptionForm{CategoryID: 0})
view.Set("hasProxyConfigured", config.Opts.HasHTTPClientProxyConfigured())
view.Set("hasProxyConfigured", config.Opts.HasHTTPClientProxyURLConfigured())
html.OK(w, r, view.Render("add_subscription"))
}
+1 -1
View File
@@ -53,7 +53,7 @@ func (h *handler) bookmarklet(w http.ResponseWriter, r *http.Request) {
view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
view.Set("defaultUserAgent", config.Opts.HTTPClientUserAgent())
view.Set("hasProxyConfigured", config.Opts.HasHTTPClientProxyConfigured())
view.Set("hasProxyConfigured", config.Opts.HasHTTPClientProxyURLConfigured())
html.OK(w, r, view.Render("add_subscription"))
}
+1
View File
@@ -63,6 +63,7 @@ func (h *handler) showChooseSubscriptionPage(w http.ResponseWriter, r *http.Requ
UrlRewriteRules: subscriptionForm.UrlRewriteRules,
FetchViaProxy: subscriptionForm.FetchViaProxy,
DisableHTTP2: subscriptionForm.DisableHTTP2,
ProxyURL: subscriptionForm.ProxyURL,
})
if localizedError != nil {
view.Set("form", subscriptionForm)
+9 -4
View File
@@ -12,6 +12,7 @@ import (
"miniflux.app/v2/internal/http/route"
"miniflux.app/v2/internal/locale"
"miniflux.app/v2/internal/model"
"miniflux.app/v2/internal/proxyrotator"
"miniflux.app/v2/internal/reader/fetcher"
feedHandler "miniflux.app/v2/internal/reader/handler"
"miniflux.app/v2/internal/reader/subscription"
@@ -41,7 +42,7 @@ func (h *handler) submitSubscription(w http.ResponseWriter, r *http.Request) {
v.Set("countUnread", h.store.CountUnreadEntries(user.ID))
v.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
v.Set("defaultUserAgent", config.Opts.HTTPClientUserAgent())
v.Set("hasProxyConfigured", config.Opts.HasHTTPClientProxyConfigured())
v.Set("hasProxyConfigured", config.Opts.HasHTTPClientProxyURLConfigured())
subscriptionForm := form.NewSubscriptionForm(r)
if validationErr := subscriptionForm.Validate(); validationErr != nil {
@@ -58,11 +59,13 @@ func (h *handler) submitSubscription(w http.ResponseWriter, r *http.Request) {
requestBuilder := fetcher.NewRequestBuilder()
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
requestBuilder.WithProxyRotator(proxyrotator.ProxyRotatorInstance)
requestBuilder.WithCustomFeedProxyURL(subscriptionForm.ProxyURL)
requestBuilder.WithCustomApplicationProxyURL(config.Opts.HTTPClientProxyURL())
requestBuilder.UseCustomApplicationProxyURL(subscriptionForm.FetchViaProxy)
requestBuilder.WithUserAgent(subscriptionForm.UserAgent, config.Opts.HTTPClientUserAgent())
requestBuilder.WithCookie(subscriptionForm.Cookie)
requestBuilder.WithUsernameAndPassword(subscriptionForm.Username, subscriptionForm.Password)
requestBuilder.UseProxy(subscriptionForm.FetchViaProxy)
requestBuilder.IgnoreTLSErrors(subscriptionForm.AllowSelfSignedCertificates)
requestBuilder.DisableHTTP2(subscriptionForm.DisableHTTP2)
@@ -105,6 +108,7 @@ func (h *handler) submitSubscription(w http.ResponseWriter, r *http.Request) {
UrlRewriteRules: subscriptionForm.UrlRewriteRules,
FetchViaProxy: subscriptionForm.FetchViaProxy,
DisableHTTP2: subscriptionForm.DisableHTTP2,
ProxyURL: subscriptionForm.ProxyURL,
},
})
if localizedError != nil {
@@ -132,6 +136,7 @@ func (h *handler) submitSubscription(w http.ResponseWriter, r *http.Request) {
UrlRewriteRules: subscriptionForm.UrlRewriteRules,
FetchViaProxy: subscriptionForm.FetchViaProxy,
DisableHTTP2: subscriptionForm.DisableHTTP2,
ProxyURL: subscriptionForm.ProxyURL,
})
if localizedError != nil {
v.Set("form", subscriptionForm)
@@ -149,7 +154,7 @@ func (h *handler) submitSubscription(w http.ResponseWriter, r *http.Request) {
view.Set("user", user)
view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
view.Set("hasProxyConfigured", config.Opts.HasHTTPClientProxyConfigured())
view.Set("hasProxyConfigured", config.Opts.HasHTTPClientProxyURLConfigured())
html.OK(w, r, view.Render("choose_subscription"))
}
+1 -1
View File
@@ -108,7 +108,7 @@ func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool) {
uiRouter.HandleFunc("/entry/bookmark/{entryID}", handler.toggleBookmark).Name("toggleBookmark").Methods(http.MethodPost)
// Share pages.
uiRouter.HandleFunc("/entry/share/{entryID}", handler.createSharedEntry).Name("shareEntry").Methods(http.MethodGet)
uiRouter.HandleFunc("/entry/share/{entryID}", handler.createSharedEntry).Name("shareEntry").Methods(http.MethodPost)
uiRouter.HandleFunc("/entry/unshare/{entryID}", handler.unshareEntry).Name("unshareEntry").Methods(http.MethodPost)
uiRouter.HandleFunc("/share/{shareCode}", handler.sharedEntry).Name("sharedEntry").Methods(http.MethodGet)
uiRouter.HandleFunc("/shares", handler.sharedEntries).Name("sharedEntries").Methods(http.MethodGet)
+9 -7
View File
@@ -10,7 +10,7 @@ import (
)
// ValidateCategoryCreation validates category creation.
func ValidateCategoryCreation(store *storage.Storage, userID int64, request *model.CategoryRequest) *locale.LocalizedError {
func ValidateCategoryCreation(store *storage.Storage, userID int64, request *model.CategoryCreationRequest) *locale.LocalizedError {
if request.Title == "" {
return locale.NewLocalizedError("error.title_required")
}
@@ -23,13 +23,15 @@ func ValidateCategoryCreation(store *storage.Storage, userID int64, request *mod
}
// ValidateCategoryModification validates category modification.
func ValidateCategoryModification(store *storage.Storage, userID, categoryID int64, request *model.CategoryRequest) *locale.LocalizedError {
if request.Title == "" {
return locale.NewLocalizedError("error.title_required")
}
func ValidateCategoryModification(store *storage.Storage, userID, categoryID int64, request *model.CategoryModificationRequest) *locale.LocalizedError {
if request.Title != nil {
if *request.Title == "" {
return locale.NewLocalizedError("error.title_required")
}
if store.AnotherCategoryExists(userID, categoryID, request.Title) {
return locale.NewLocalizedError("error.category_already_exists")
if store.AnotherCategoryExists(userID, categoryID, *request.Title) {
return locale.NewLocalizedError("error.category_already_exists")
}
}
return nil
+14
View File
@@ -35,6 +35,10 @@ func ValidateFeedCreation(store *storage.Storage, userID int64, request *model.F
return locale.NewLocalizedError("error.feed_invalid_keeplist_rule")
}
if request.ProxyURL != "" && !IsValidURL(request.ProxyURL) {
return locale.NewLocalizedError("error.invalid_feed_proxy_url")
}
return nil
}
@@ -88,5 +92,15 @@ func ValidateFeedModification(store *storage.Storage, userID, feedID int64, requ
}
}
if request.ProxyURL != nil {
if *request.ProxyURL == "" {
return locale.NewLocalizedError("error.proxy_url_not_empty")
}
if !IsValidURL(*request.ProxyURL) {
return locale.NewLocalizedError("error.invalid_feed_proxy_url")
}
}
return nil
}
+4
View File
@@ -14,5 +14,9 @@ func ValidateSubscriptionDiscovery(request *model.SubscriptionDiscoveryRequest)
return locale.NewLocalizedError("error.invalid_site_url")
}
if request.ProxyURL != "" && !IsValidURL(request.ProxyURL) {
return locale.NewLocalizedError("error.invalid_proxy_url")
}
return nil
}
-8
View File
@@ -44,13 +44,5 @@ func (w *Worker) Run(c <-chan model.Job) {
}
metric.BackgroundFeedRefreshDuration.WithLabelValues(status).Observe(time.Since(startTime).Seconds())
}
if localizedError != nil {
slog.Warn("Unable to refresh a feed",
slog.Int64("user_id", job.UserID),
slog.Int64("feed_id", job.FeedID),
slog.Any("error", localizedError.Error()),
)
}
}
}
+20 -4
View File
@@ -1,12 +1,13 @@
.\" Manpage for miniflux.
.TH "MINIFLUX" "1" "December 7, 2024" "\ \&" "\ \&"
.TH "MINIFLUX" "1" "April 11, 2025" "\ \&" "\ \&"
.SH NAME
miniflux \- Minimalist and opinionated feed reader
.SH SYNOPSIS
\fBminiflux\fR [-vic] [-config-dump] [-config-file] [-create-admin] [-debug] [-flush-sessions]
[-healthcheck] [-info] [-migrate] [-refresh-feeds] [-reset-feed-errors] [-reset-password]
\fBminiflux\fR [-vic] [-config-dump] [-config-file] [-create-admin] [-debug]
[-flush-sessions] [-healthcheck] [-info] [-migrate] [-refresh-feeds]
[-reset-feed-errors] [-reset-feed-next-check-at] [-reset-password]
[-run-cleanup-tasks] [-version]
.SH DESCRIPTION
@@ -83,6 +84,11 @@ Refresh a batch of feeds and exit\&.
Clear all feed errors for all users\&.
.RE
.PP
.B \-reset-feed-next-check-at
.RS 4
Reset the next check time for all feeds\&.
.RE
.PP
.B \-reset-password
.RS 4
Reset user password\&.
@@ -293,8 +299,13 @@ Maximum body size for HTTP requests in Mebibyte (MiB)\&.
.br
Default is 15 MiB\&.
.TP
.B HTTP_CLIENT_PROXIES
Enable proxy rotation for outgoing requests by providing a comma-separated list of proxy URLs\&.
.br
Default is empty\&.
.TP
.B HTTP_CLIENT_PROXY
Proxy URL for HTTP client\&.
Proxy URL to use when the "Fetch via proxy" feed option is enabled\&.
.br
Default is empty\&.
.TP
@@ -528,6 +539,11 @@ Minimum interval in minutes for the entry frequency scheduler\&.
.br
Default is 5 minutes\&.
.TP
.B SCHEDULER_ROUND_ROBIN_MAX_INTERVAL
Maximum interval in minutes for the round robin scheduler\&.
.br
Default is 1440 minutes (24 hours)\&.
.TP
.B SCHEDULER_ROUND_ROBIN_MIN_INTERVAL
Minimum interval in minutes for the round robin scheduler\&.
.br