Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f68046cce0 | |||
| d33e305af9 | |||
| 764212f37c | |||
| e02b65d4bc | |||
| fe7ec25a09 | |||
| 2959a4d2bf | |||
| 6b70a7dc81 | |||
| 495c6aacd9 | |||
| bc228d0afe | |||
| d139d8a6ce | |||
| 28d0185e79 | |||
| c87c93d85f | |||
| 0ef21e85c2 | |||
| 7438d67061 | |||
| e4399b4f9a | |||
| b4f891705a | |||
| 3d72404ace | |||
| c4e92e6111 | |||
| ef22e95f8b | |||
| 7b344de846 | |||
| c45b51d1f8 | |||
| 0af1a6e121 | |||
| 535fd050b7 | |||
| d20e8a4e2c | |||
| 7514e8a0c1 | |||
| ca3ede3183 | |||
| 038d33600a | |||
| 1a3a174688 | |||
| 6848c759b5 | |||
| 10578f512f |
@@ -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
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
-----------------------------
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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ä"
|
||||
}
|
||||
}
|
||||
@@ -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 d’accès",
|
||||
"page.settings.webauthn.register": "Enregister une nouvelle clé d’accès",
|
||||
"page.settings.webauthn.register.error": "Impossible d'enregistrer la clé d’accè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"
|
||||
}
|
||||
}
|
||||
@@ -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 के रूप में लॉग इन किया"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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ı"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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 分类"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -20,3 +20,7 @@ func OptionalString(value string) *string {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func SetOptionalField[T any](value T) *T {
|
||||
return &value
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user