Merge branch 'FreshRSS:master' into master
This commit is contained in:
commit
699a771e48
35 changed files with 2559 additions and 2386 deletions
|
|
@ -1,7 +0,0 @@
|
|||
.git/
|
||||
*.min.js
|
||||
node_modules/
|
||||
symbolic/
|
||||
third-party/
|
||||
tmp/
|
||||
vendor/
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
{
|
||||
"env": {
|
||||
"browser": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"standard"
|
||||
],
|
||||
"rules": {
|
||||
"camelcase": "off",
|
||||
"comma-dangle": ["warn", {
|
||||
"arrays": "always-multiline",
|
||||
"objects": "always-multiline"
|
||||
}],
|
||||
"eqeqeq": "off",
|
||||
"indent": ["warn", "tab", { "SwitchCase": 1 }],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"max-len": ["warn", 165],
|
||||
"no-tabs": "off",
|
||||
"semi": ["warn", "always"],
|
||||
"space-before-function-paren": ["warn", {
|
||||
"anonymous": "always",
|
||||
"named": "never",
|
||||
"asyncArrow": "always"
|
||||
}],
|
||||
"yoda": "off"
|
||||
},
|
||||
"root": true
|
||||
}
|
||||
|
|
@ -6,11 +6,12 @@
|
|||
"line-length": false,
|
||||
"no-hard-tabs": false,
|
||||
"no-inline-html": {
|
||||
"allowed_elements": ["br", "kbd"]
|
||||
"allowed_elements": ["br", "img", "kbd"]
|
||||
},
|
||||
"no-multiple-blanks": {
|
||||
"maximum": 2
|
||||
},
|
||||
"no-trailing-spaces": true,
|
||||
"ul-indent": false,
|
||||
"ul-style": {
|
||||
"style": "consistent"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"plugins": [
|
||||
"stylelint-order",
|
||||
"stylelint-scss",
|
||||
"stylelint-stylistic"
|
||||
"@stylistic/stylelint-plugin"
|
||||
],
|
||||
"rules": {
|
||||
"at-rule-empty-line-before": [
|
||||
|
|
@ -11,27 +11,27 @@
|
|||
"ignoreAtRules": [ "after-comment", "else" ]
|
||||
}
|
||||
],
|
||||
"stylistic/at-rule-name-space-after": [
|
||||
"@stylistic/at-rule-name-space-after": [
|
||||
"always", {
|
||||
"ignoreAtRules": [ "after-comment" ]
|
||||
}
|
||||
],
|
||||
"stylistic/block-closing-brace-newline-after": [
|
||||
"@stylistic/block-closing-brace-newline-after": [
|
||||
"always", {
|
||||
"ignoreAtRules": [ "if", "else" ]
|
||||
}
|
||||
],
|
||||
"stylistic/block-closing-brace-newline-before": "always-multi-line",
|
||||
"stylistic/block-opening-brace-newline-after": "always-multi-line",
|
||||
"stylistic/block-opening-brace-space-before": "always",
|
||||
"stylistic/color-hex-case": "lower",
|
||||
"@stylistic/block-closing-brace-newline-before": "always-multi-line",
|
||||
"@stylistic/block-opening-brace-newline-after": "always-multi-line",
|
||||
"@stylistic/block-opening-brace-space-before": "always",
|
||||
"@stylistic/color-hex-case": "lower",
|
||||
"color-hex-length": "short",
|
||||
"color-no-invalid-hex": true,
|
||||
"stylistic/declaration-colon-space-after": "always",
|
||||
"stylistic/declaration-colon-space-before": "never",
|
||||
"stylistic/indentation": "tab",
|
||||
"@stylistic/declaration-colon-space-after": "always",
|
||||
"@stylistic/declaration-colon-space-before": "never",
|
||||
"@stylistic/indentation": "tab",
|
||||
"no-descending-specificity": null,
|
||||
"stylistic/no-eol-whitespace": true,
|
||||
"@stylistic/no-eol-whitespace": true,
|
||||
"property-no-vendor-prefix": true,
|
||||
"rule-empty-line-before": [
|
||||
"always", {
|
||||
|
|
|
|||
15
README.md
15
README.md
|
|
@ -56,11 +56,13 @@ There are some FreshRSS extensions out there, developed by community members:
|
|||
|
||||
### By [@CN-Tools](https://github.com/cn-tools)
|
||||
|
||||
* [Black List](https://github.com/cn-tools/cntools_FreshRssExtensions/tree/master/xExtension-BlackList): Blacklist to block feeds for users
|
||||
* [Copy 2 Clipboard](https://github.com/cn-tools/cntools_FreshRssExtensions/tree/master/xExtension-Copy2Clipboard): Add a button in the navigation bar to copy the destination links of all visible entries into clipboard
|
||||
* [Feed Title Builder](https://github.com/cn-tools/cntools_FreshRssExtensions/tree/master/xExtension-FeedTitleBuilder): Build your own feed title based on url, the original feed title and the date the feed was added
|
||||
* [FilterTitle](https://github.com/cn-tools/cntools_FreshRssExtensions/tree/master/xExtension-FilterTitle): Filter out feed entries by keywords parsed by the feed entry title
|
||||
* [RemoveEmojis](https://github.com/cn-tools/cntools_FreshRssExtensions/tree/master/xExtension-RemoveEmojis): Remove emojis in the title of newly added feed entries.
|
||||
* [YouTube Channel 2 RSSFeed](https://github.com/cn-tools/cntools_FreshRssExtensions/tree/master/xExtension-YouTubeChannel2RssFeed): You can add a YouTube Channel URL and will get it as RSSFeed
|
||||
* [RemoveEmojis](https://github.com/cn-tools/cntools_FreshRssExtensions/tree/master/xExtension-RemoveEmojis): Remove emojis in the title of newly added feed entries
|
||||
* [SendToMyJD2](https://github.com/cn-tools/cntools_FreshRssExtensions/tree/master/xExtension-SendToMyJD2): Send links to a jDownloader2 instance with the myJDownloader2 API
|
||||
* [YouTube Channel 2 RSSFeed](https://github.com/cn-tools/cntools_FreshRssExtensions/tree/master/xExtension-YouTubeChannel2RssFeed): You can add a YouTube Channel URL and will get it as RSSFeed. Additional you can detect YouTube shorts.
|
||||
|
||||
### By [@DevonHess](https://github.com/DevonHess)
|
||||
|
||||
|
|
@ -82,6 +84,11 @@ There are some FreshRSS extensions out there, developed by community members:
|
|||
|
||||
* [Pocket Button](https://github.com/christian-putzke/freshrss-pocket-button): Add articles to Pocket with one simple button click or a keyboard shortcut.
|
||||
|
||||
### By [@Joedmin](https://github.com/Joedmin/)
|
||||
|
||||
* [Readeck Button](https://github.com/Joedmin/xExtension-readeck-button): Add articles to a selected Readeck instance with one simple button click or a keyboard shortcut.
|
||||
* [Wallabag Button](https://github.com/Joedmin/xExtension-wallabag-button): Add articles to a selected Wallabag instance with one simple button click or a keyboard shortcut.
|
||||
|
||||
### By [@printfuck](https://github.com/printfuck/)
|
||||
|
||||
* [Readable](https://github.com/printfuck/xExtension-Readable): Fetch article content for selected feeds with [Readability](https://github.com/mozilla/readability) or [Mercury](https://github.com/postlight/mercury-parser)
|
||||
|
|
@ -134,3 +141,7 @@ There are some FreshRSS extensions out there, developed by community members:
|
|||
### By [@kalvn](https://github.com/kalvn)
|
||||
|
||||
* [Mark Previous as Read](https://github.com/kalvn/freshrss-mark-previous-as-read): Adds a button in the footer of each entry. Clicking this button will mark all previous entries belonging to the current feed, as read.
|
||||
|
||||
### By [@lukasMega](https://github.com/lukasMega)
|
||||
|
||||
* [Word Highlighter](https://github.com/lukasMega/Extensions-FreshRSS-): Gives you ability to highlight user-defined words (using [mark.js](https://github.com/julkue/mark.js))
|
||||
|
|
|
|||
|
|
@ -49,8 +49,8 @@
|
|||
"ext-phar": "*",
|
||||
"ext-tokenizer": "*",
|
||||
"ext-xmlwriter": "*",
|
||||
"phpstan/phpstan": "^1.10",
|
||||
"phpstan/phpstan-strict-rules": "^1.5",
|
||||
"phpstan/phpstan": "^1.11",
|
||||
"phpstan/phpstan-strict-rules": "^1.6",
|
||||
"squizlabs/php_codesniffer": "^3.9"
|
||||
},
|
||||
"scripts": {
|
||||
|
|
|
|||
42
composer.lock
generated
42
composer.lock
generated
|
|
@ -4,21 +4,21 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "bf0d2d1a05ed08841ca6bd3e7ec96b74",
|
||||
"content-hash": "52101009acffc9684a721cc20ec9e731",
|
||||
"packages": [],
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "1.10.66",
|
||||
"version": "1.11.10",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan.git",
|
||||
"reference": "94779c987e4ebd620025d9e5fdd23323903950bd"
|
||||
"reference": "640410b32995914bde3eed26fa89552f9c2c082f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/94779c987e4ebd620025d9e5fdd23323903950bd",
|
||||
"reference": "94779c987e4ebd620025d9e5fdd23323903950bd",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/640410b32995914bde3eed26fa89552f9c2c082f",
|
||||
"reference": "640410b32995914bde3eed26fa89552f9c2c082f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -61,31 +61,27 @@
|
|||
{
|
||||
"url": "https://github.com/phpstan",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-03-28T16:17:31+00:00"
|
||||
"time": "2024-08-08T09:02:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan-strict-rules",
|
||||
"version": "1.5.3",
|
||||
"version": "1.6.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan-strict-rules.git",
|
||||
"reference": "568210bd301f94a0d4b1e5a0808c374c1b9cf11b"
|
||||
"reference": "363f921dd8441777d4fc137deb99beb486c77df1"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/568210bd301f94a0d4b1e5a0808c374c1b9cf11b",
|
||||
"reference": "568210bd301f94a0d4b1e5a0808c374c1b9cf11b",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/363f921dd8441777d4fc137deb99beb486c77df1",
|
||||
"reference": "363f921dd8441777d4fc137deb99beb486c77df1",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2 || ^8.0",
|
||||
"phpstan/phpstan": "^1.10.60"
|
||||
"phpstan/phpstan": "^1.11"
|
||||
},
|
||||
"require-dev": {
|
||||
"nikic/php-parser": "^4.13.0",
|
||||
|
|
@ -114,22 +110,22 @@
|
|||
"description": "Extra strict and opinionated rules for PHPStan",
|
||||
"support": {
|
||||
"issues": "https://github.com/phpstan/phpstan-strict-rules/issues",
|
||||
"source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.5.3"
|
||||
"source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.6.0"
|
||||
},
|
||||
"time": "2024-04-06T07:43:25+00:00"
|
||||
"time": "2024-04-20T06:37:51+00:00"
|
||||
},
|
||||
{
|
||||
"name": "squizlabs/php_codesniffer",
|
||||
"version": "3.9.1",
|
||||
"version": "3.10.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git",
|
||||
"reference": "267a4405fff1d9c847134db3a3c92f1ab7f77909"
|
||||
"reference": "86e5f5dd9a840c46810ebe5ff1885581c42a3017"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/267a4405fff1d9c847134db3a3c92f1ab7f77909",
|
||||
"reference": "267a4405fff1d9c847134db3a3c92f1ab7f77909",
|
||||
"url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/86e5f5dd9a840c46810ebe5ff1885581c42a3017",
|
||||
"reference": "86e5f5dd9a840c46810ebe5ff1885581c42a3017",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -196,7 +192,7 @@
|
|||
"type": "open_collective"
|
||||
}
|
||||
],
|
||||
"time": "2024-03-31T21:03:09+00:00"
|
||||
"time": "2024-07-21T23:26:44+00:00"
|
||||
}
|
||||
],
|
||||
"aliases": [],
|
||||
|
|
@ -233,5 +229,5 @@
|
|||
"ext-tokenizer": "*",
|
||||
"ext-xmlwriter": "*"
|
||||
},
|
||||
"plugin-api-version": "2.3.0"
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
|
|
|
|||
55
eslint.config.js
Normal file
55
eslint.config.js
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import globals from "globals";
|
||||
import js from "@eslint/js";
|
||||
import neostandard, { resolveIgnoresFromGitignore } from 'neostandard';
|
||||
import stylistic from '@stylistic/eslint-plugin';
|
||||
|
||||
export default [
|
||||
{
|
||||
files: ["**/*.js"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
},
|
||||
sourceType: "script",
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: [
|
||||
...resolveIgnoresFromGitignore(),
|
||||
"**/*.min.js",
|
||||
"extensions/",
|
||||
"p/scripts/vendor/",
|
||||
],
|
||||
},
|
||||
js.configs.recommended,
|
||||
// stylistic.configs['recommended-flat'],
|
||||
...neostandard(),
|
||||
{
|
||||
plugins: {
|
||||
"@stylistic": stylistic,
|
||||
},
|
||||
rules: {
|
||||
"camelcase": "off",
|
||||
"eqeqeq": "off",
|
||||
"no-empty": ["error", { "allowEmptyCatch": true }],
|
||||
"no-unused-vars": ["error", {
|
||||
"args": "none",
|
||||
"caughtErrors": "none",
|
||||
}],
|
||||
"object-shorthand": "off",
|
||||
"yoda": "off",
|
||||
"@stylistic/indent": ["warn", "tab", { "SwitchCase": 1 }],
|
||||
"@stylistic/linebreak-style": ["error", "unix"],
|
||||
"@stylistic/max-len": ["warn", 165],
|
||||
"@stylistic/no-tabs": "off",
|
||||
"@stylistic/quotes": ["off", "single", { "avoidEscape": true }],
|
||||
"@stylistic/quote-props": ["warn", "consistent"],
|
||||
"@stylistic/semi": ["warn", "always"],
|
||||
"@stylistic/space-before-function-paren": ["warn", {
|
||||
"anonymous": "always",
|
||||
"asyncArrow": "always",
|
||||
"named": "never",
|
||||
}],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
"name": "AutoTTL",
|
||||
"author": "Magnus Kokk",
|
||||
"description": "A FreshRSS extension for automatic feed refresh TTL based on the average frequency of entries.",
|
||||
"version": "0.5.5",
|
||||
"version": "0.5.8",
|
||||
"entrypoint": "AutoTTL",
|
||||
"type": "user",
|
||||
"url": "https://github.com/mgnsk/FreshRSS-AutoTTL",
|
||||
|
|
@ -93,7 +93,7 @@
|
|||
"name": "Custom CSS",
|
||||
"author": "Marien Fressinaud",
|
||||
"description": "Give possibility to overwrite the CSS with a user-specific rules.",
|
||||
"version": "0.5.1",
|
||||
"version": "0.5.2",
|
||||
"entrypoint": "CustomCSS",
|
||||
"type": "user",
|
||||
"url": "https://github.com/FreshRSS/Extensions",
|
||||
|
|
@ -104,7 +104,7 @@
|
|||
"name": "Custom JS",
|
||||
"author": "Frans de Jonge",
|
||||
"description": "Apply custom JS.",
|
||||
"version": "0.5.1",
|
||||
"version": "0.5.2",
|
||||
"entrypoint": "CustomJS",
|
||||
"type": "user",
|
||||
"url": "https://github.com/FreshRSS/Extensions",
|
||||
|
|
@ -170,7 +170,7 @@
|
|||
"name": "FreshRss FlareSolverr",
|
||||
"author": "James Ravenscroft",
|
||||
"description": "Use a Flaresolverr instance to bypass cloudflare security checks",
|
||||
"version": "0.1",
|
||||
"version": "0.2",
|
||||
"entrypoint": "FlareSolverr",
|
||||
"type": "system",
|
||||
"url": "https://github.com/ravenscroftj/freshrss-flaresolverr-extension",
|
||||
|
|
@ -192,7 +192,7 @@
|
|||
"name": "Image Cache",
|
||||
"author": "Victrid",
|
||||
"description": "Cache feed images on your own facility or Cloudflare cache.",
|
||||
"version": "0.3",
|
||||
"version": "0.4.0",
|
||||
"entrypoint": "ImageCache",
|
||||
"type": "user",
|
||||
"url": "https://github.com/Victrid/freshrss-image-cache-plugin",
|
||||
|
|
@ -203,7 +203,7 @@
|
|||
"name": "Image Proxy",
|
||||
"author": "Frans de Jonge",
|
||||
"description": "No insecure content warnings or disappearing images.",
|
||||
"version": "0.7.2",
|
||||
"version": "0.7.3",
|
||||
"entrypoint": "ImageProxy",
|
||||
"type": "user",
|
||||
"url": "https://github.com/FreshRSS/Extensions",
|
||||
|
|
@ -236,7 +236,7 @@
|
|||
"name": "Kagi Summarizer",
|
||||
"author": "Rudis Muiznieks",
|
||||
"description": "Add buttons to summarize articles with the Kagi Universal Summarizer.",
|
||||
"version": "0.2",
|
||||
"version": "0.3",
|
||||
"entrypoint": "KagiSummarizer",
|
||||
"type": "user",
|
||||
"url": "https://code.sitosis.com/rudism/freshrss-kagi-summarizer",
|
||||
|
|
@ -268,8 +268,8 @@
|
|||
{
|
||||
"name": "Mark Previous as Read",
|
||||
"author": "kalvn",
|
||||
"description": "This extension adds a button in the footer of each entry. Clicking this button will mark all previous entries belonging to the current feed, as read. The goal is, when going through a very long list of entries without reading them all, to be able to stop and continue later.",
|
||||
"version": "1.0.1",
|
||||
"description": "This extension adds a button in the footer of each entry. Clicking this button will mark all previous entries as read.",
|
||||
"version": "1.1.1",
|
||||
"entrypoint": "MarkPreviousAsRead",
|
||||
"type": "user",
|
||||
"url": "https://github.com/kalvn/freshrss-mark-previous-as-read",
|
||||
|
|
@ -291,7 +291,7 @@
|
|||
"name": "News Assistant",
|
||||
"author": "Mervyn Zhan",
|
||||
"description": "Using the ai api of `OpenAI`, `Anthropic`, `Groq` by [Portkey-AI/gateway](https://github.com/Portkey-AI/gateway/) to summary the news.",
|
||||
"version": "0.11.0",
|
||||
"version": "0.11.2",
|
||||
"entrypoint": "NewsAssistant",
|
||||
"type": "system",
|
||||
"url": "https://github.com/reply2future/xExtension-NewsAssistant",
|
||||
|
|
@ -302,7 +302,7 @@
|
|||
"name": "Pocket Button",
|
||||
"author": "Christian Putzke",
|
||||
"description": "Add articles to Pocket with one simple button click or a keyboard shortcut.",
|
||||
"version": "0.4",
|
||||
"version": "0.5",
|
||||
"entrypoint": "PocketButton",
|
||||
"type": "user",
|
||||
"url": "https://github.com/christian-putzke/freshrss-pocket-button",
|
||||
|
|
@ -313,7 +313,7 @@
|
|||
"name": "Quick Collapse",
|
||||
"author": "romibi and Marien Fressinaud",
|
||||
"description": "Quickly change from folded to unfolded articles",
|
||||
"version": "0.2.1",
|
||||
"version": "0.2.2",
|
||||
"entrypoint": "QuickCollapse",
|
||||
"type": "user",
|
||||
"url": "https://github.com/FreshRSS/Extensions",
|
||||
|
|
@ -335,13 +335,24 @@
|
|||
"name": "Readable",
|
||||
"author": "printfuck",
|
||||
"description": "Fetch article content for selected feeds with readability or mercury",
|
||||
"version": "0.2",
|
||||
"version": "0.3",
|
||||
"entrypoint": "Readable",
|
||||
"type": "user",
|
||||
"url": "https://github.com/printfuck/xExtension-Readable",
|
||||
"method": "git",
|
||||
"directory": "."
|
||||
},
|
||||
{
|
||||
"name": "Readeck Button",
|
||||
"author": "Joedmin",
|
||||
"description": "Add articles to Readeck with one simple button click or a keyboard shortcut.",
|
||||
"version": "0.6",
|
||||
"entrypoint": "ReadeckButton",
|
||||
"type": "user",
|
||||
"url": "https://github.com/Joedmin/xExtension-readeck-button",
|
||||
"method": "git",
|
||||
"directory": "."
|
||||
},
|
||||
{
|
||||
"name": "Reading Time",
|
||||
"author": "Lapineige",
|
||||
|
|
@ -386,11 +397,22 @@
|
|||
"method": "git",
|
||||
"directory": "xExtension-RemoveEmojis"
|
||||
},
|
||||
{
|
||||
"name": "SendToMyJD2",
|
||||
"author": "CNTools | Clemens Neubauer",
|
||||
"description": "Send links to a jDownloader2 instance with the myJDownloader2 API",
|
||||
"version": "0.0.1-alpha",
|
||||
"entrypoint": "SendToMyJD2",
|
||||
"type": "user",
|
||||
"url": "https://github.com/cn-tools/cntools_FreshRssExtensions",
|
||||
"method": "git",
|
||||
"directory": "xExtension-SendToMyJD2"
|
||||
},
|
||||
{
|
||||
"name": "Share By Email",
|
||||
"author": "Marien Fressinaud",
|
||||
"description": "Improve the sharing by email system.",
|
||||
"version": "0.2.2",
|
||||
"version": "0.2.3",
|
||||
"entrypoint": "ShareByEmail",
|
||||
"type": "user",
|
||||
"url": "https://github.com/FreshRSS/Extensions",
|
||||
|
|
@ -400,8 +422,8 @@
|
|||
{
|
||||
"name": "ShowFeedID",
|
||||
"author": "math-GH",
|
||||
"description": "Show the feed ID",
|
||||
"version": "0.2.1",
|
||||
"description": "Show the ID of feed and category",
|
||||
"version": "0.3.0",
|
||||
"entrypoint": "ShowFeedID",
|
||||
"type": "user",
|
||||
"url": "https://github.com/FreshRSS/Extensions",
|
||||
|
|
@ -452,17 +474,6 @@
|
|||
"method": "git",
|
||||
"directory": "."
|
||||
},
|
||||
{
|
||||
"name": "TinyTinyRSS API",
|
||||
"author": "Marien Fressinaud",
|
||||
"description": "Provides an API compliant with TinyTinyRSS applications.",
|
||||
"version": "0.1",
|
||||
"entrypoint": "TTRSS_API",
|
||||
"type": "system",
|
||||
"url": "https://github.com/FreshRSS/Extensions",
|
||||
"method": "git",
|
||||
"directory": "xExtension-TTRSS_API"
|
||||
},
|
||||
{
|
||||
"name": "Title-Wrap",
|
||||
"author": "₣rans de Jonge, Joris Kinable",
|
||||
|
|
@ -508,6 +519,17 @@
|
|||
"method": "git",
|
||||
"directory": "."
|
||||
},
|
||||
{
|
||||
"name": "Wallabag Button",
|
||||
"author": "Joedmin",
|
||||
"description": "Add articles to Wallabag with one simple button click or a keyboard shortcut.",
|
||||
"version": "0.2",
|
||||
"entrypoint": "WallabagButton",
|
||||
"type": "user",
|
||||
"url": "https://github.com/Joedmin/xExtension-wallabag-button",
|
||||
"method": "git",
|
||||
"directory": "."
|
||||
},
|
||||
{
|
||||
"name": "White List",
|
||||
"author": "Alexis Degrugillier",
|
||||
|
|
@ -519,11 +541,22 @@
|
|||
"method": "git",
|
||||
"directory": "."
|
||||
},
|
||||
{
|
||||
"name": "Word highlighter",
|
||||
"author": "Lukas Melega",
|
||||
"description": "Highlight specific words",
|
||||
"version": "0.0.2",
|
||||
"entrypoint": "WordHighlighter",
|
||||
"type": "user",
|
||||
"url": "https://github.com/FreshRSS/Extensions",
|
||||
"method": "git",
|
||||
"directory": "xExtension-WordHighlighter"
|
||||
},
|
||||
{
|
||||
"name": "YouTube Video Feed",
|
||||
"author": "Kevin Papst",
|
||||
"description": "Embed YouTube feeds inside article content.",
|
||||
"version": "0.11",
|
||||
"version": "0.12",
|
||||
"entrypoint": "YouTube",
|
||||
"type": "user",
|
||||
"url": "https://github.com/FreshRSS/Extensions",
|
||||
|
|
@ -534,7 +567,7 @@
|
|||
"name": "YouTubeChannel2RssFeed",
|
||||
"author": "CNTools | Clemens Neubauer",
|
||||
"description": "Transfer YouTube URL into RSS Feed URL.",
|
||||
"version": "0.6.0-alpha",
|
||||
"version": "0.6.1",
|
||||
"entrypoint": "YouTubeChannel2RssFeed",
|
||||
"type": "user",
|
||||
"url": "https://github.com/cn-tools/cntools_FreshRssExtensions",
|
||||
|
|
|
|||
3459
package-lock.json
generated
3459
package-lock.json
generated
File diff suppressed because it is too large
Load diff
30
package.json
30
package.json
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"name": "freshrss-extensions",
|
||||
"type": "module",
|
||||
"description": "Extensions for FreshRSS",
|
||||
"homepage": "https://freshrss.org/",
|
||||
"readmeFilename": "README.md",
|
||||
|
|
@ -16,11 +17,11 @@
|
|||
},
|
||||
"license": "see each extension",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
},
|
||||
"scripts": {
|
||||
"eslint": "eslint --ext .js .",
|
||||
"eslint_fix": "eslint --fix --ext .js .",
|
||||
"eslint": "eslint .",
|
||||
"eslint_fix": "eslint --fix .",
|
||||
"markdownlint": "markdownlint '**/*.md'",
|
||||
"markdownlint_fix": "markdownlint --fix '**/*.md'",
|
||||
"rtlcss": "npm run symbolic && rtlcss -d symbolic/ && find -L symbolic/ -type f -name '*.rtl.rtl.css' -delete",
|
||||
|
|
@ -31,18 +32,17 @@
|
|||
"fix": "npm run rtlcss && npm run stylelint_fix && npm run eslint_fix && npm run markdownlint_fix"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.54.0",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-plugin-import": "^2.29.0",
|
||||
"eslint-plugin-n": "^16.3.1",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"markdownlint-cli": "^0.37.0",
|
||||
"rtlcss": "^4.1.1",
|
||||
"sass": "^1.69.5",
|
||||
"stylelint": "^15.11.0",
|
||||
"stylelint-config-recommended-scss": "^13.1.0",
|
||||
"stylelint-order": "^6.0.3",
|
||||
"stylelint-stylistic": "^0.4.3"
|
||||
"eslint": "^9.8.0",
|
||||
"@eslint/js": "^9.8.0",
|
||||
"globals": "^15.9.0",
|
||||
"markdownlint-cli": "^0.41.0",
|
||||
"neostandard": "^0.11.2",
|
||||
"rtlcss": "^4.2.0",
|
||||
"sass": "^1.77.8",
|
||||
"stylelint": "^16.8.1",
|
||||
"stylelint-config-recommended-scss": "^14.1.0",
|
||||
"stylelint-order": "^6.0.4",
|
||||
"@stylistic/stylelint-plugin": "^3.0.0"
|
||||
},
|
||||
"rtlcssConfig": {}
|
||||
}
|
||||
|
|
|
|||
162
phpcs.xml
162
phpcs.xml
|
|
@ -1,119 +1,113 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ruleset name="FreshRSS Ruleset">
|
||||
<description>Created with the PHP Coding Standard Generator. https://edorian.github.com/php-coding-standard-generator/</description>
|
||||
<arg name="extensions" value="php,phtml,css,js"/>
|
||||
<ruleset name="FreshRSS">
|
||||
<arg name="extensions" value="php,phtml"/>
|
||||
<arg name="tab-width" value="4"/>
|
||||
<exclude-pattern>./lib/SimplePie/</exclude-pattern>
|
||||
<exclude-pattern>./lib/PHPMailer/</exclude-pattern>
|
||||
<exclude-pattern>./lib/http-conditional.php</exclude-pattern>
|
||||
<exclude-pattern>./lib/lib_phpQuery.php</exclude-pattern>
|
||||
<exclude-pattern>./node_modules/</exclude-pattern>
|
||||
<exclude-pattern>./.git/</exclude-pattern>
|
||||
<exclude-pattern>./data/config.php</exclude-pattern>
|
||||
<exclude-pattern>./data/update.php</exclude-pattern>
|
||||
<exclude-pattern>./data/users/*/config.php</exclude-pattern>
|
||||
<exclude-pattern>./extensions/</exclude-pattern>
|
||||
<exclude-pattern>./lib/http-conditional.php</exclude-pattern>
|
||||
<exclude-pattern>./lib/marienfressinaud/</exclude-pattern>
|
||||
<exclude-pattern>./lib/phpgt/</exclude-pattern>
|
||||
<exclude-pattern>./lib/phpmailer/</exclude-pattern>
|
||||
<exclude-pattern>./lib/simplepie/</exclude-pattern>
|
||||
<exclude-pattern>./node_modules/</exclude-pattern>
|
||||
<exclude-pattern>./p/scripts/vendor/</exclude-pattern>
|
||||
<exclude-pattern>./vendor/</exclude-pattern>
|
||||
<exclude-pattern>*.min.js$</exclude-pattern>
|
||||
<!-- Duplicate class names are not allowed -->
|
||||
<!-- Additional exclusions for Extensions: -->
|
||||
<exclude-pattern>./symbolic/</exclude-pattern>
|
||||
<exclude-pattern>./third-party/</exclude-pattern>
|
||||
<exclude-pattern>./tmp/</exclude-pattern>
|
||||
<rule ref="PSR12">
|
||||
<exclude name="Generic.ControlStructures.InlineControlStructure.NotAllowed"/>
|
||||
<exclude name="Generic.Formatting.DisallowMultipleStatements.SameLine"/>
|
||||
<exclude name="Generic.WhiteSpace.DisallowTabIndent.NonIndentTabsUsed"/>
|
||||
<exclude name="Generic.WhiteSpace.DisallowTabIndent.TabsUsed"/>
|
||||
<exclude name="Generic.WhiteSpace.DisallowTabIndent.TabsUsedHeredocCloser"/>
|
||||
<exclude name="PSR1.Classes.ClassDeclaration.MissingNamespace"/>
|
||||
<exclude name="PSR1.Classes.ClassDeclaration.MultipleClasses"/>
|
||||
<exclude name="PSR1.Files.SideEffects.FoundWithSymbols"/>
|
||||
<exclude name="PSR1.Methods.CamelCapsMethodName.NotCamelCaps"/>
|
||||
<exclude name="PSR12.Classes.OpeningBraceSpace.Found"/><!-- Consider using PSR12 defaults instead -->
|
||||
<exclude name="PSR12.ControlStructures.ControlStructureSpacing.CloseParenthesisLine"/>
|
||||
<exclude name="PSR12.ControlStructures.ControlStructureSpacing.FirstExpressionLine"/><!-- Consider using PSR12 defaults instead -->
|
||||
<exclude name="PSR12.Files.FileHeader.IncorrectOrder"/><!-- Consider using PSR12 defaults instead -->
|
||||
<exclude name="PSR12.Files.FileHeader.SpacingAfterBlock"/><!-- Consider using PSR12 defaults instead -->
|
||||
<exclude name="PSR12.Traits.UseDeclaration.MultipleImport"/>
|
||||
<exclude name="PSR2.Classes.ClassDeclaration.CloseBraceAfterBody"/><!-- Consider using PSR12 defaults instead -->
|
||||
<exclude name="PSR2.Classes.ClassDeclaration.OpenBraceNewLine"/><!-- Consider using PSR12 defaults instead -->
|
||||
<exclude name="PSR2.ControlStructures.SwitchDeclaration.BodyOnNextLineCASE"/>
|
||||
<exclude name="PSR2.ControlStructures.SwitchDeclaration.BreakNotNewLine"/>
|
||||
<exclude name="PSR2.Functions.FunctionCallSignature.ContentAfterOpenBracket"/>
|
||||
<exclude name="PSR2.Methods.FunctionCallSignature.CloseBracketLine"/>
|
||||
<exclude name="PSR2.Methods.FunctionCallSignature.ContentAfterOpenBracket"/>
|
||||
<exclude name="PSR2.Methods.FunctionCallSignature.Indent"/>
|
||||
<exclude name="PSR2.Methods.FunctionCallSignature.MultipleArguments"/>
|
||||
<exclude name="PSR2.Methods.MethodDeclaration.Underscore"/>
|
||||
<exclude name="Squiz.Classes.ValidClassName.NotCamelCaps"/>
|
||||
<exclude name="Squiz.Functions.MultiLineFunctionDeclaration.BraceOnSameLine"/>
|
||||
<exclude name="Squiz.Functions.MultiLineFunctionDeclaration.CloseBracketLine"/>
|
||||
<exclude name="Squiz.Functions.MultiLineFunctionDeclaration.ContentAfterBrace"/>
|
||||
<exclude name="Squiz.Functions.MultiLineFunctionDeclaration.FirstParamSpacing"/>
|
||||
<exclude name="Squiz.Functions.MultiLineFunctionDeclaration.Indent"/>
|
||||
<exclude name="Squiz.Functions.MultiLineFunctionDeclaration.OneParamPerLine"/>
|
||||
<exclude name="Squiz.WhiteSpace.ScopeClosingBrace.ContentBefore"/>
|
||||
</rule>
|
||||
<rule ref="Generic.Classes.DuplicateClassName"/>
|
||||
<!-- Statements must not be empty -->
|
||||
<rule ref="Generic.CodeAnalysis.EmptyStatement"/>
|
||||
<!-- Unconditional if-statements are not allowed -->
|
||||
<rule ref="Generic.CodeAnalysis.UnconditionalIfStatement"/>
|
||||
<!-- Do not use final statements inside final classes -->
|
||||
<rule ref="Generic.CodeAnalysis.UnnecessaryFinalModifier"/>
|
||||
<!-- Do not override methods to call their parent -->
|
||||
<rule ref="Generic.CodeAnalysis.UselessOverridingMethod"/>
|
||||
<!-- Maximum line length -->
|
||||
<rule ref="Generic.Files.LineLength">
|
||||
<!-- For language strings maximum line lengths make little sense. -->
|
||||
<exclude-pattern>./app/i18n/</exclude-pattern>
|
||||
<!-- Don’t enforce line length on the HTML; the point is to improve legibility, not reduce it -->
|
||||
<exclude-pattern>*.phtml$</exclude-pattern>
|
||||
<properties>
|
||||
<property name="lineLimit" value="165"/>
|
||||
<property name="absoluteLineLimit" value="190"/>
|
||||
</properties>
|
||||
<exclude-pattern>/app/i18n/*\.php$</exclude-pattern>
|
||||
<exclude-pattern>*\.phtml$</exclude-pattern>
|
||||
</rule>
|
||||
<!-- When calling a function: -->
|
||||
<!-- Do not add a space before the opening parenthesis -->
|
||||
<!-- Do not add a space after the opening parenthesis -->
|
||||
<!-- Do not add a space before the closing parenthesis -->
|
||||
<!-- Do not add a space before a comma -->
|
||||
<!-- Add a space after a comma -->
|
||||
<rule ref="Generic.Functions.FunctionCallArgumentSpacing"/>
|
||||
<rule ref="Generic.PHP.DisallowShortOpenTag">
|
||||
<exclude name="Generic.PHP.DisallowShortOpenTag.EchoFound"/>
|
||||
</rule>
|
||||
<rule ref="Generic.PHP.DeprecatedFunctions" />
|
||||
<!-- Use UPPERCARE for constants -->
|
||||
<rule ref="Generic.NamingConventions.UpperCaseConstantName"/>
|
||||
<!-- Use lowercase for 'true', 'false' and 'null' -->
|
||||
<rule ref="Generic.PHP.LowerCaseConstant"/>
|
||||
<!-- Use a single string instead of concatenating -->
|
||||
<rule ref="Generic.Functions.OpeningFunctionBraceKernighanRitchie"/><!-- Consider using PSR12 defaults instead -->
|
||||
<rule ref="Generic.PHP.DeprecatedFunctions"/>
|
||||
<rule ref="Generic.Strings.UnnecessaryStringConcat">
|
||||
<properties>
|
||||
<!-- Allow string concatenating across multiple lines -->
|
||||
<property name="allowMultiline" value="true"/>
|
||||
</properties>
|
||||
</rule>
|
||||
<!-- Use tabs for indentation -->
|
||||
<rule ref="Generic.WhiteSpace.DisallowSpaceIndent"/>
|
||||
<!-- Parameters with default values must appear last in functions -->
|
||||
<rule ref="PEAR.Functions.ValidDefaultValue"/>
|
||||
<!-- Use 'elseif' instead of 'else if' -->
|
||||
<rule ref="PSR2.ControlStructures.ElseIfDeclaration"/>
|
||||
<!-- Do not add spaces after opening or before closing bracket -->
|
||||
<rule ref="PSR2.ControlStructures.ControlStructureSpacing"/>
|
||||
<!-- Add a new line at the end of a file -->
|
||||
<rule ref="PSR2.Files.EndFileNewline"/>
|
||||
<!-- Use Unix newlines -->
|
||||
<rule ref="Generic.Files.LineEndings">
|
||||
<properties>
|
||||
<property name="eolChar" value="\n" />
|
||||
</properties>
|
||||
<rule ref="Generic.WhiteSpace.ScopeIndent.Incorrect">
|
||||
<exclude-pattern>*\.phtml$</exclude-pattern>
|
||||
<exclude-pattern>/app/install.php</exclude-pattern>
|
||||
</rule>
|
||||
<!-- Add space after closing parenthesis -->
|
||||
<!-- Add body into new line -->
|
||||
<!-- Close body in new line -->
|
||||
<rule ref="Generic.WhiteSpace.ScopeIndent.IncorrectExact">
|
||||
<exclude-pattern>*\.phtml$</exclude-pattern>
|
||||
<exclude-pattern>/app/install.php</exclude-pattern>
|
||||
</rule>
|
||||
<rule ref="Internal.NoCodeFound">
|
||||
<exclude-pattern>*\.phtml$</exclude-pattern>
|
||||
</rule>
|
||||
<!-- <rule ref="Squiz.Commenting.ClassComment.Missing"/> --><!-- Consider adding -->
|
||||
|
||||
<rule ref="Squiz.ControlStructures.ControlSignature">
|
||||
<!-- No space after keyword (before opening parenthesis) -->
|
||||
<exclude name="Squiz.ControlStructures.ControlSignature.SpaceAfterKeyword"/>
|
||||
</rule>
|
||||
<!-- When declaring a function: -->
|
||||
<!-- Do not add a space before a comma -->
|
||||
<!-- Add a space after a comma -->
|
||||
<!-- Add a space before and after an equal sign -->
|
||||
<rule ref="Squiz.Functions.FunctionDeclarationArgumentSpacing">
|
||||
<include-pattern>*\.phtml$</include-pattern>
|
||||
<properties>
|
||||
<property name="equalsSpacing" value="1"/>
|
||||
<property name="requiredSpacesBeforeColon" value="0" />
|
||||
</properties>
|
||||
</rule>
|
||||
<!-- Do not add spaces when casting -->
|
||||
<rule ref="Squiz.WhiteSpace.CastSpacing"/>
|
||||
<!-- Operators must have a space around them -->
|
||||
<rule ref="Squiz.ControlStructures.ControlSignature">
|
||||
<include-pattern>*\.php$</include-pattern>
|
||||
</rule>
|
||||
|
||||
<rule ref="Squiz.ControlStructures.ControlSignature.NewlineAfterOpenBrace">
|
||||
<exclude-pattern>*\.phtml$</exclude-pattern>
|
||||
</rule>
|
||||
<rule ref="Squiz.WhiteSpace.OperatorSpacing">
|
||||
<properties>
|
||||
<property name="ignoreNewlines" value="true" />
|
||||
<property name="ignoreNewlines" value="true"/>
|
||||
</properties>
|
||||
</rule>
|
||||
<!-- Do not add a whitespace before a semicolon -->
|
||||
<rule ref="Squiz.WhiteSpace.ScopeClosingBrace.Indent">
|
||||
<exclude-pattern>*\.phtml$</exclude-pattern>
|
||||
</rule>
|
||||
<rule ref="Squiz.WhiteSpace.SemicolonSpacing"/>
|
||||
<!-- Do not add whitespace at start or end of a file or end of a line -->
|
||||
<rule ref="Squiz.WhiteSpace.SuperfluousWhitespace"/>
|
||||
<!-- Expected space after closing parenthesis -->
|
||||
<rule ref="Squiz.ControlStructures.ControlSignature.SpaceAfterCloseParenthesis">
|
||||
<exclude-pattern>.phtml$</exclude-pattern>
|
||||
</rule>
|
||||
<!-- Opening brace on same line as function declaration -->
|
||||
<rule ref="Generic.Functions.OpeningFunctionBraceKernighanRitchie" />
|
||||
<!-- Newline required after opening brace -->
|
||||
<rule ref="Squiz.ControlStructures.ControlSignature.NewlineAfterOpenBrace">
|
||||
<exclude-pattern>.phtml$</exclude-pattern>
|
||||
<exclude-pattern>.js$</exclude-pattern>
|
||||
</rule>
|
||||
<!-- No PHP code was found in this file -->
|
||||
<rule ref="Internal.NoCodeFound">
|
||||
<exclude-pattern>.phtml$</exclude-pattern>
|
||||
</rule>
|
||||
</ruleset>
|
||||
|
|
|
|||
|
|
@ -7,4 +7,3 @@ parameters:
|
|||
analyse:
|
||||
- xExtension-ImageProxy/configure.phtml
|
||||
- xExtension-ImageProxy/extension.php
|
||||
- xExtension-TTRSS_API/ttrss.php
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
parameters:
|
||||
level: 0
|
||||
treatPhpDocTypesAsCertain: false
|
||||
fileExtensions:
|
||||
- php
|
||||
- phtml
|
||||
|
|
@ -22,3 +21,4 @@ parameters:
|
|||
dynamicConstantNames:
|
||||
- TYPE_GIT
|
||||
reportMaybesInPropertyPhpDocTypes: false
|
||||
treatPhpDocTypesAsCertain: false
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ parameters:
|
|||
# TODO: Increase rule-level https://phpstan.org/user-guide/rule-levels
|
||||
level: 1
|
||||
phpVersion: 80399 # TODO: Remove line when moving composer.json to PHP 8+
|
||||
treatPhpDocTypesAsCertain: false
|
||||
fileExtensions:
|
||||
- php
|
||||
- phtml
|
||||
|
|
@ -23,6 +22,7 @@ parameters:
|
|||
- TYPE_GIT
|
||||
checkMissingOverrideMethodAttribute: true
|
||||
reportMaybesInPropertyPhpDocTypes: false
|
||||
treatPhpDocTypesAsCertain: false
|
||||
strictRules:
|
||||
allRules: false
|
||||
booleansInConditions: true
|
||||
|
|
@ -37,5 +37,12 @@ parameters:
|
|||
strictCalls: true
|
||||
switchConditionsMatchingType: true
|
||||
uselessCast: true
|
||||
exceptions:
|
||||
check:
|
||||
missingCheckedExceptionInThrows: false # TODO pass
|
||||
tooWideThrowType: true
|
||||
implicitThrows: false
|
||||
checkedExceptionClasses:
|
||||
- 'Minz_Exception'
|
||||
includes:
|
||||
- vendor/phpstan/phpstan-strict-rules/rules.neon
|
||||
|
|
|
|||
|
|
@ -56,6 +56,12 @@
|
|||
"url": "https://github.com/christian-putzke/freshrss-pocket-button",
|
||||
"type": "git"
|
||||
}, {
|
||||
"url": "https://github.com/Joedmin/xExtension-readeck-button",
|
||||
"type": "git"
|
||||
}, {
|
||||
"url": "https://github.com/Joedmin/xExtension-wallabag-button",
|
||||
"type": "git"
|
||||
},{
|
||||
"url": "https://github.com/printfuck/xExtension-Readable",
|
||||
"type": "git"
|
||||
}, {
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
# TinyTinyRSS API extension
|
||||
|
||||
This extension provides a TTRSS-compliant API. It means with this extension you can use applications made for TTRSS initially.
|
||||
|
||||
Note the API is NOT FULLY SUPPORTED YET!
|
||||
|
||||
Please be sure to enable API in the configuration and set an API password on your profile.
|
||||
|
||||
URL to use is something like <http://rss.example.com/api/ttrss.php> or <http://example.com/freshrss/p/api/ttrss.php>.
|
||||
|
||||
To use it, upload this directory in your `./extensions` directory and enable it on the extension panel in FreshRSS.
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
final class TTRSS_APIExtension extends Minz_Extension {
|
||||
|
||||
#[\Override]
|
||||
public function init(): void {
|
||||
$this->registerHook('post_update', [$this, 'postUpdateHook']);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function install() {
|
||||
$filename = 'ttrss.php';
|
||||
$file_source = join_path($this->getPath(), $filename);
|
||||
$path_destination = join_path(PUBLIC_PATH, 'api');
|
||||
$file_destination = join_path($path_destination, $filename);
|
||||
|
||||
if (!is_writable($path_destination)) {
|
||||
return 'server cannot write in ' . $path_destination;
|
||||
}
|
||||
|
||||
if (file_exists($file_destination)) {
|
||||
if (!unlink($file_destination)) {
|
||||
return 'API file seems already existing but cannot be removed';
|
||||
}
|
||||
}
|
||||
|
||||
if (!file_exists($file_source)) {
|
||||
return 'API file seems not existing in this extension. Try to download it again.';
|
||||
}
|
||||
|
||||
if (!copy($file_source, $file_destination)) {
|
||||
return 'the API file has failed during installation.';
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function uninstall() {
|
||||
$filename = 'ttrss.php';
|
||||
$file_destination = join_path(PUBLIC_PATH, 'api', $filename);
|
||||
|
||||
if (file_exists($file_destination) && !unlink($file_destination)) {
|
||||
return 'API file cannot be removed';
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function postUpdateHook(): void {
|
||||
$res = $this->install();
|
||||
|
||||
if ($res !== true) {
|
||||
Minz_Log::warning('Problem during TTRSS API extension post update: ' . $res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"name": "TinyTinyRSS API",
|
||||
"author": "Marien Fressinaud",
|
||||
"description": "Provides an API compliant with TinyTinyRSS applications.",
|
||||
"version": "0.1",
|
||||
"entrypoint": "TTRSS_API",
|
||||
"type": "system"
|
||||
}
|
||||
|
|
@ -1,537 +0,0 @@
|
|||
<?php
|
||||
|
||||
require('../../constants.php');
|
||||
require(LIB_PATH . '/lib_rss.php'); // Includes class autoloader
|
||||
|
||||
|
||||
final class MyPDO extends Minz_ModelPdo {
|
||||
}
|
||||
|
||||
|
||||
final class FreshAPI_TTRSS {
|
||||
const API_LEVEL = 11;
|
||||
|
||||
const STATUS_OK = 0;
|
||||
const STATUS_ERR = 1;
|
||||
|
||||
private $seq = 0;
|
||||
private $user = '';
|
||||
private $method = 'index';
|
||||
private $params = array();
|
||||
private $system_conf = null;
|
||||
private $user_conf = null;
|
||||
|
||||
public function __construct($params) {
|
||||
$this->seq = isset($params['seq']) ? $params['seq'] : 0;
|
||||
$this->user = Minz_Session::paramString('currentUser');
|
||||
$this->method = $params['op'];
|
||||
$this->params = $params;
|
||||
$this->system_conf = Minz_Configuration::get('system');
|
||||
if ($this->user != '') {
|
||||
$this->user_conf = get_user_configuration($this->user);
|
||||
}
|
||||
}
|
||||
|
||||
public function param($param, $default = false) {
|
||||
if (isset($this->params[$param])) {
|
||||
return $this->params[$param];
|
||||
} else {
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
public function good($reply) {
|
||||
$this->response($reply, self::STATUS_OK);
|
||||
}
|
||||
|
||||
public function bad($reply) {
|
||||
$this->response($reply, self::STATUS_ERR);
|
||||
}
|
||||
|
||||
private function response($reply, $status) {
|
||||
header('Content-Type: text/json; charset=utf-8');
|
||||
$result = json_encode(array(
|
||||
'seq' => $this->seq,
|
||||
'status' => $status,
|
||||
'content' => $reply,
|
||||
));
|
||||
|
||||
// Minz_Log::debug($result);
|
||||
print($result);
|
||||
exit();
|
||||
}
|
||||
|
||||
public function handle() {
|
||||
if (!$this->system_conf->api_enabled) {
|
||||
$this->bad(array(
|
||||
'error' => 'API_DISABLED'
|
||||
));
|
||||
}
|
||||
|
||||
if ($this->user === '' &&
|
||||
!in_array($this->method, ['login', 'isloggedin'], true)) {
|
||||
$this->bad(array(
|
||||
'error' => 'NOT_LOGGED_IN'
|
||||
));
|
||||
}
|
||||
|
||||
if (is_callable(array($this, $this->method))) {
|
||||
call_user_func(array($this, $this->method));
|
||||
} else {
|
||||
Minz_Log::warning('TTRSS API: ' . $this->method . '() method does not exist');
|
||||
}
|
||||
}
|
||||
|
||||
private function auth_user($username, $password) {
|
||||
if (!function_exists('password_verify')) {
|
||||
include_once(LIB_PATH . '/password_compat.php');
|
||||
}
|
||||
|
||||
$user_conf = get_user_configuration($username);
|
||||
if (is_null($user_conf)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($user_conf->apiPasswordHash != '' &&
|
||||
password_verify($password, $user_conf->apiPasswordHash)) {
|
||||
Minz_Session::_param('currentUser', $username);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function getApiLevel() {
|
||||
$this->good(array(
|
||||
'level' => self::API_LEVEL
|
||||
));
|
||||
}
|
||||
|
||||
public function getVersion() {
|
||||
$this->good(array(
|
||||
'version' => FRESHRSS_VERSION
|
||||
));
|
||||
}
|
||||
|
||||
public function login() {
|
||||
$username = $this->param('user');
|
||||
$password = $this->param('password');
|
||||
$password_base64 = base64_decode($this->param('password'), true);
|
||||
|
||||
if ($this->auth_user($username, $password) ||
|
||||
$this->auth_user($username, $password_base64)) {
|
||||
$this->good(array(
|
||||
'session_id' => session_id(),
|
||||
'api_level' => self::API_LEVEL,
|
||||
));
|
||||
} else {
|
||||
Minz_Log::warning('TTRSS API: invalid user login: ' . $username);
|
||||
$this->bad(array(
|
||||
'error' => 'LOGIN_ERROR'
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
public function logout() {
|
||||
Minz_Session::_param('currentUser');
|
||||
$this->good(array(
|
||||
'status' => 'OK'
|
||||
));
|
||||
}
|
||||
|
||||
public function isLoggedIn() {
|
||||
$this->good(array(
|
||||
'status' => $this->user !== ''
|
||||
));
|
||||
}
|
||||
|
||||
public function getCategories() {
|
||||
$unread_only = $this->param('unread_only', false);
|
||||
$include_empty = $this->param('include_empty', true);
|
||||
// $enable_nested = $this->param('enable_nested', true); // not supported
|
||||
|
||||
$pdo = new MyPDO();
|
||||
$sql = <<<SQL
|
||||
SELECT DISTINCT c.id, c.name, COUNT(f.id) AS nb_feeds,
|
||||
(SELECT COUNT(e.id) FROM entry e WHERE e.id_feed = f.id AND e.is_read=0) AS unread
|
||||
FROM `_category` c
|
||||
LEFT JOIN `_feed` f ON c.id = f.category
|
||||
GROUP BY c.id, c.name
|
||||
SQL;
|
||||
$stm = $pdo->prepare($sql);
|
||||
$stm->execute();
|
||||
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$caterogies = array();
|
||||
foreach ($res as $cat) {
|
||||
if ($unread_only && $cat['unread'] <= 0 ||
|
||||
!$include_empty && $cat['nb_feeds'] <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$caterogies[] = array(
|
||||
'id' => $cat['id'],
|
||||
'title' => $cat['name'],
|
||||
'unread' => $cat['unread'],
|
||||
);
|
||||
}
|
||||
|
||||
$this->good($caterogies);
|
||||
}
|
||||
|
||||
public function getFeeds() {
|
||||
$cat_id = $this->param('cat_id');
|
||||
$unread_only = $this->param('unread_only', false);
|
||||
$limit = (int)$this->param('limit', -1);
|
||||
$offset = (int)$this->param('offset', -1);
|
||||
// $include_nested = $this->param('include_nested', false) === true; // not supported
|
||||
|
||||
$sql_values = array();
|
||||
|
||||
$sql_where = '';
|
||||
if ($cat_id >= 0) {
|
||||
// special ids are not supported (yet?)
|
||||
$sql_where = ' WHERE f.category = ?';
|
||||
$sql_values[] = $cat_id;
|
||||
}
|
||||
|
||||
$sql_limit = '';
|
||||
if ($limit >= 0 && $offset >= 0) {
|
||||
$sql_limit = ' LIMIT ? OFFSET ?';
|
||||
$sql_values[] = $limit;
|
||||
$sql_values[] = $offset;
|
||||
}
|
||||
|
||||
$pdo = new MyPDO();
|
||||
$sql = 'SELECT f.id, f.name, f.url, f.category, f.cache_nbUnreads AS unread, f.lastUpdate'
|
||||
. ' FROM `%_feed` f'
|
||||
. $sql_where
|
||||
. $sql_limit;
|
||||
$stm = $pdo->prepare($sql);
|
||||
$stm->execute($sql_values);
|
||||
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$feeds = array();
|
||||
foreach ($res as $feed) {
|
||||
if ($unread_only && $feed['unread'] <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$feeds[] = array(
|
||||
'id' => $feed['id'],
|
||||
'title' => $feed['name'],
|
||||
'feed_url' => $feed['url'],
|
||||
'unread' => $feed['unread'],
|
||||
'has_icon' => true,
|
||||
'cat_id' => $feed['category'],
|
||||
'last_updated' => $feed['lastUpdate']
|
||||
);
|
||||
}
|
||||
|
||||
$this->good($feeds);
|
||||
}
|
||||
|
||||
public function getHeadlines() {
|
||||
$feed_id = $this->param('feed_id');
|
||||
if ($feed_id === false) {
|
||||
$this->bad(array(
|
||||
'error' => 'INCORRECT_USAGE'
|
||||
));
|
||||
}
|
||||
|
||||
$limit = min(200, (int)$this->param('limit', 200));
|
||||
$offset = (int)$this->param('skip', 0);
|
||||
$is_cat = $this->param('is_cat', true);
|
||||
$show_excerpt = $this->param('show_excerpt', false);
|
||||
$show_content = $this->param('show_content', true);
|
||||
$view_mode = $this->param('view_mode', 'all_articles');
|
||||
$since_id = $this->param('since_id', '');
|
||||
$order_by = $this->param('order_by', 'feed_dates');
|
||||
$search = $this->param('search', '');
|
||||
// $filter = $this->param('filter'); // not supported
|
||||
// $include_attachments = $this->param('include_attachments'); // not supported
|
||||
// $include_nested = $this->param('include_nested'); // not supported
|
||||
// $force_update = $this->param('force_update', false); // not supported
|
||||
// $sanitize = $this->param('sanitize', true); // not supported
|
||||
// $has_sandbox = $this->param('has_sandbox', false); // not supported
|
||||
// $search_mode = $this->param('search_mode', 'all_feeds'); // not supported
|
||||
// $match_on = $this->param('match_on'); // not supported
|
||||
|
||||
// Get the current state
|
||||
$state = 0;
|
||||
switch ($view_mode) {
|
||||
case 'unread':
|
||||
case 'adaptive':
|
||||
$state = FreshRSS_Entry::STATE_NOT_READ;
|
||||
break;
|
||||
case 'marked':
|
||||
$state = FreshRSS_Entry::STATE_FAVORITE;
|
||||
break;
|
||||
case 'updated': // not supported
|
||||
case 'all_articles':
|
||||
default:
|
||||
$state = FreshRSS_Entry::STATE_ALL;
|
||||
}
|
||||
|
||||
// Get the current type
|
||||
$id = '';
|
||||
$type = 'A';
|
||||
switch ($feed_id) {
|
||||
case 0: // archived (not supported)
|
||||
case -1: // starred
|
||||
$type = 's';
|
||||
break;
|
||||
case -2: // published (not supported)
|
||||
case -3: // fresh (not supported)
|
||||
case -4: // all articles
|
||||
// nothing to do
|
||||
break;
|
||||
default:
|
||||
// if ($is_cat) {
|
||||
// $type = 'c';
|
||||
// } else {
|
||||
// $type = 'f';
|
||||
// }
|
||||
$type = 'f';
|
||||
$id = $feed_id;
|
||||
}
|
||||
|
||||
// Get the order
|
||||
$order = 'DESC';
|
||||
if ($order_by === 'date_reverse') {
|
||||
$order = 'ASC';
|
||||
}
|
||||
|
||||
// Fix the limit: since we don't have any mechanism of offset in
|
||||
// listWhere, it will be done in PHP. Limit has to be increased by
|
||||
// the value of offset.
|
||||
$limit += $offset;
|
||||
|
||||
$entryDAO = FreshRSS_Factory::createEntryDao();
|
||||
$entries = $entryDAO->listWhere($type, $id, $state, $order, $limit, $since_id, $search, $since_id);
|
||||
|
||||
$headlines = array();
|
||||
$nb_items = 0;
|
||||
foreach ($entries as $item) {
|
||||
if ($nb_items < $offset) {
|
||||
$nb_items++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$feed = $item->feed(true);
|
||||
$line = array(
|
||||
'id' => $item->id(),
|
||||
'unread' => !$item->isRead(),
|
||||
'marked' => $item->isFavorite(),
|
||||
'published' => true,
|
||||
'updated' => $item->date(true),
|
||||
'is_updated' => false,
|
||||
'title' => $item->title(),
|
||||
'link' => $item->link(),
|
||||
'tags' => $item->tags(),
|
||||
'author' => $item->authors(true),
|
||||
'feed_id' => $feed->id(),
|
||||
'feed_title' => $feed->name(),
|
||||
);
|
||||
|
||||
if ($show_excerpt) {
|
||||
// @todo add a facultative max char in content method to get
|
||||
// an excerpt.
|
||||
$line['excerpt'] = $item->content();
|
||||
}
|
||||
|
||||
if ($show_content) {
|
||||
$line['content'] = $item->content();
|
||||
}
|
||||
|
||||
$headlines[] = $line;
|
||||
}
|
||||
|
||||
$this->good($headlines);
|
||||
}
|
||||
|
||||
public function updateArticle() {
|
||||
$article_ids = $this->param('article_ids', '');
|
||||
$mode = $this->param('mode'); // 0 set to false, 1 set to true,
|
||||
// 2 toggle but not supported.
|
||||
$field = $this->param('field');
|
||||
// $data = $this->param('data'); // not supported
|
||||
|
||||
$article_ids = explode(',', $article_ids);
|
||||
$entryDAO = FreshRSS_Factory::createEntryDao();
|
||||
$number_article_updated = 0;
|
||||
switch ($field) {
|
||||
case 0: // starred
|
||||
$number_article_updated = $entryDAO->markFavorite($article_ids, $mode);
|
||||
break;
|
||||
case 2: // unread
|
||||
$number_article_updated = $entryDAO->markRead($article_ids, !$mode);
|
||||
break;
|
||||
case 1: // published (not supported)
|
||||
case 3: // article note (not supported)
|
||||
default:
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
$this->good(array(
|
||||
'status' => 'OK',
|
||||
'updated' => $number_article_updated,
|
||||
));
|
||||
}
|
||||
|
||||
public function catchupFeed() {
|
||||
$id = $this->param('feed_id');
|
||||
$is_cat = $this->param('is_cat', true);
|
||||
|
||||
$entryDAO = FreshRSS_Factory::createEntryDao();
|
||||
switch ($id) {
|
||||
case -1: // starred
|
||||
$entryDAO->markReadEntries(0, true);
|
||||
break;
|
||||
case -4: // all articles
|
||||
$entryDAO->markReadEntries();
|
||||
break;
|
||||
case 0: // archived (not supported)
|
||||
case -2: // published (not supported)
|
||||
case -3: // fresh (not supported)
|
||||
break;
|
||||
default:
|
||||
// if ($is_cat) {
|
||||
// $entryDAO->markReadCat($id);
|
||||
// } else {
|
||||
// $entryDAO->markReadFeed($id);
|
||||
// }
|
||||
$entryDAO->markReadFeed($id);
|
||||
}
|
||||
|
||||
$this->good(array(
|
||||
'status' => 'OK'
|
||||
));
|
||||
}
|
||||
|
||||
public function getCounters() {
|
||||
$categoryDAO = new FreshRSS_CategoryDAO();
|
||||
$counters = array();
|
||||
$total_unreads = 0;
|
||||
|
||||
// Get feed unread counters
|
||||
$categories = $categoryDAO->listCategories(true, true);
|
||||
foreach ($categories as $cat) {
|
||||
foreach ($cat->feeds() as $feed) {
|
||||
$counters[] = array(
|
||||
'id' => $feed->id(),
|
||||
'counter' => $feed->nbNotRead(),
|
||||
);
|
||||
}
|
||||
|
||||
$total_unreads += $cat->nbNotRead();
|
||||
}
|
||||
|
||||
// Get global unread counter
|
||||
$counters[] = array(
|
||||
'id' => 'global-unread',
|
||||
'counter' => $total_unreads,
|
||||
);
|
||||
|
||||
// Get favorite unread counter
|
||||
$entryDAO = FreshRSS_Factory::createEntryDao();
|
||||
$fav_counters = $entryDAO->countUnreadReadFavorites();
|
||||
$counters[] = array(
|
||||
'id' => -1,
|
||||
'counter' => $fav_counters['unread'],
|
||||
);
|
||||
|
||||
$this->good($counters);
|
||||
}
|
||||
|
||||
public function getFeedTree() {
|
||||
$include_empty = $this->param('include_empty', true);
|
||||
$tree = array(
|
||||
'identifier' => 'id',
|
||||
'label' => 'name',
|
||||
'items' => array(),
|
||||
);
|
||||
|
||||
$categoryDAO = new FreshRSS_CategoryDAO();
|
||||
$categories = $categoryDAO->listCategories(true, true);
|
||||
foreach ($categories as $cat) {
|
||||
$tree_cat = array(
|
||||
'id' => 'CAT:' . $cat->id(),
|
||||
'name' => $cat->name(),
|
||||
'unread' => $cat->nbNotRead(),
|
||||
'type' => 'category',
|
||||
'bare_id' => $cat->id(),
|
||||
'items' => array(),
|
||||
);
|
||||
|
||||
foreach ($cat->feeds() as $feed) {
|
||||
$tree_cat['items'][] = array(
|
||||
'id' => 'FEED:' . $feed->id(),
|
||||
'name' => $feed->name(),
|
||||
'unread' => $feed->nbNotRead(),
|
||||
'type' => 'feed',
|
||||
'error' => $feed->inError(),
|
||||
'updated' => $feed->lastUpdate(),
|
||||
'bare_id' => $feed->id(),
|
||||
);
|
||||
}
|
||||
|
||||
if (count($tree_cat['items']) > 0 || $include_empty) {
|
||||
$tree['items'][] = $tree_cat;
|
||||
}
|
||||
}
|
||||
|
||||
$this->good(array(
|
||||
'categories' => $tree
|
||||
));
|
||||
}
|
||||
|
||||
public function getUnread() {
|
||||
Minz_Log::warning('TTRSS API: getUnread() not implemented');
|
||||
}
|
||||
public function getArticle() {
|
||||
Minz_Log::warning('TTRSS API: getArticle() not implemented');
|
||||
}
|
||||
public function getConfig() {
|
||||
Minz_Log::warning('TTRSS API: getConfig() not implemented');
|
||||
}
|
||||
public function updateFeed() {
|
||||
Minz_Log::warning('TTRSS API: updateFeed() not implemented');
|
||||
}
|
||||
public function getPref() {
|
||||
Minz_Log::warning('TTRSS API: getPref() not implemented');
|
||||
}
|
||||
public function getLabels() {
|
||||
Minz_Log::warning('TTRSS API: getLabels() not implemented');
|
||||
}
|
||||
public function setArticleLabel() {
|
||||
Minz_Log::warning('TTRSS API: setArticleLabel() not implemented');
|
||||
}
|
||||
public function shareToPublish() {
|
||||
Minz_Log::warning('TTRSS API: shareToPublish() not implemented');
|
||||
}
|
||||
public function subscribeToFeed() {
|
||||
Minz_Log::warning('TTRSS API: subscribeToFeed() not implemented');
|
||||
}
|
||||
public function unsubscribeFeed() {
|
||||
Minz_Log::warning('TTRSS API: unsubscribeFeed() not implemented');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Minz_Configuration::register('system',
|
||||
DATA_PATH . '/config.php',
|
||||
DATA_PATH . '/config.default.php');
|
||||
|
||||
$input = file_get_contents("php://input");
|
||||
// Minz_Log::debug($input);
|
||||
$input = json_decode($input, true);
|
||||
|
||||
if (isset($input["sid"])) {
|
||||
session_id($input["sid"]);
|
||||
}
|
||||
|
||||
Minz_Session::init('FreshRSS');
|
||||
|
||||
$api = new FreshAPI_TTRSS($input);
|
||||
$api->handle();
|
||||
30
xExtension-WordHighlighter/README.md
Normal file
30
xExtension-WordHighlighter/README.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# WordHighlighter extension
|
||||
|
||||
A FreshRSS extension which give ability to highlight user-defined words.
|
||||
|
||||
## Usage
|
||||
|
||||
To use it, upload this directory in your `./extensions` directory and enable it on the extension panel in FreshRSS. You can add words to be highlighted by clicking on the manage button ⚙️.
|
||||
See also official docs at freshrss.github.io/FreshRSS/en/admins/15_extensions.html
|
||||
|
||||
## Preview
|
||||
|
||||
Light theme:
|
||||
|
||||

|
||||
|
||||
<!-- markdownlint-disable -->
|
||||
<details>
|
||||
<summary>click to see example screenshot in dark theme</summary>
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
<!-- markdownlint-enable -->
|
||||
|
||||
## Changelog
|
||||
|
||||
- 0.0.2 use `json` for storing configuration, add more configuration options
|
||||
(enable_in_article, enable_logs, case_sensitive, separate_word_search)
|
||||
and refactored & simplified code
|
||||
- 0.0.1 initial version (as a proper FreshRSS extension)
|
||||
88
xExtension-WordHighlighter/configure.phtml
Normal file
88
xExtension-WordHighlighter/configure.phtml
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
/** @var WordHighlighterExtension $this */
|
||||
?>
|
||||
<form action="<?= _url('extension', 'configure', 'e', urlencode($this->getName())) ?>" method="post">
|
||||
<input type="hidden" name="_csrf" value="<?= FreshRSS_Auth::csrfToken() ?>" />
|
||||
|
||||
<div class="form-group">
|
||||
<label class="group-name" for="words_list">
|
||||
<?= _t('ext.word_highlighter.write_words') ?> <br />
|
||||
<?= _t('ext.word_highlighter.write_words_more') ?>
|
||||
</label>
|
||||
<div class="group-controls">
|
||||
<textarea name="words_list"
|
||||
id="words_list"><?= $this->word_highlighter_conf ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="enable-in-article" class="group-name">
|
||||
<?= _t('ext.word_highlighter.enable_in_article') ?>
|
||||
</label>
|
||||
<div class="group-controls">
|
||||
<?php if ($this->enable_in_article == '') { ?>
|
||||
<input type="checkbox" name="enable-in-article" id="enable-in-article"></input>
|
||||
<?php } else { ?>
|
||||
<input type="checkbox" name="enable-in-article" id="enable-in-article" checked="true"></input>
|
||||
<?php } ?>
|
||||
<i><?= _t('ext.word_highlighter.enable_in_article_more') ?></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<details>
|
||||
<summary>Click to see more advanced options</summary>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="case_sensitive" class="group-name">
|
||||
<?= _t('ext.word_highlighter.case_sensitive') ?>
|
||||
</label>
|
||||
<div class="group-controls">
|
||||
<?php if ($this->case_sensitive == '') { ?>
|
||||
<input type="checkbox" name="case_sensitive" id="case_sensitive"></input>
|
||||
<?php } else { ?>
|
||||
<input type="checkbox" name="case_sensitive" id="case_sensitive" checked="true"></input>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="separate_word_search" class="group-name">
|
||||
<?= _t('ext.word_highlighter.separate_word_search') ?>
|
||||
</label>
|
||||
<div class="group-controls">
|
||||
<?php if ($this->separate_word_search == '') { ?>
|
||||
<input type="checkbox" name="separate_word_search" id="separate_word_search"></input>
|
||||
<?php } else { ?>
|
||||
<input type="checkbox" name="separate_word_search" id="separate_word_search" checked="true"></input>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="enable_logs" class="group-name">
|
||||
<?= _t('ext.word_highlighter.enable_logs') ?>
|
||||
</label>
|
||||
<div class="group-controls">
|
||||
<?php if ($this->enable_logs == '') { ?>
|
||||
<input type="checkbox" name="enable_logs" id="enable_logs"></input>
|
||||
<?php } else { ?>
|
||||
<input type="checkbox" name="enable_logs" id="enable_logs" checked="true"></input>
|
||||
<?php } ?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="form-group form-actions">
|
||||
<?php if ($this->permission_problem !== '') { ?>
|
||||
<p class="alert alert-error"><?= _t('ext.word_highlighter.permission_problem', $this->permission_problem) ?></p>
|
||||
<?php } else { ?>
|
||||
<div class="group-controls">
|
||||
<button type="submit" class="btn btn-important"><?= _t('gen.action.submit') ?></button>
|
||||
<button type="reset" class="btn"><?= _t('gen.action.cancel') ?></button>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
102
xExtension-WordHighlighter/extension.php
Normal file
102
xExtension-WordHighlighter/extension.php
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
final class WordHighlighterExtension extends Minz_Extension
|
||||
{
|
||||
const JSON_ENCODE_CONF = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR;
|
||||
|
||||
public string $word_highlighter_conf = 'test';
|
||||
public string $permission_problem = '';
|
||||
public bool $enable_in_article = false;
|
||||
public bool $enable_logs = false;
|
||||
public bool $case_sensitive = false;
|
||||
public bool $separate_word_search = false;
|
||||
|
||||
#[\Override]
|
||||
public function init(): void
|
||||
{
|
||||
$this->registerTranslates();
|
||||
|
||||
// register CSS for WordHighlighter:
|
||||
Minz_View::appendStyle($this->getFileUrl('style.css', 'css'));
|
||||
|
||||
Minz_View::appendScript($this->getFileUrl('mark.min.js', 'js'), false, false, false);
|
||||
|
||||
$current_user = Minz_Session::paramString('currentUser');
|
||||
|
||||
$staticPath = join_path($this->getPath(), 'static');
|
||||
$configFileJs = join_path($staticPath, ('config.' . $current_user . '.js'));
|
||||
|
||||
if (file_exists($configFileJs)) {
|
||||
Minz_View::appendScript($this->getFileUrl(('config.' . $current_user . '.js'), 'js'));
|
||||
}
|
||||
|
||||
Minz_View::appendScript($this->getFileUrl('word-highlighter.js', 'js'));
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function handleConfigureAction(): void
|
||||
{
|
||||
$this->registerTranslates();
|
||||
|
||||
$current_user = Minz_Session::paramString('currentUser');
|
||||
$staticPath = join_path($this->getPath(), 'static');
|
||||
|
||||
$configFileJson = join_path($staticPath, ('config.' . $current_user . '.json'));
|
||||
|
||||
if (!file_exists($configFileJson) && !is_writable($staticPath)) {
|
||||
$tmpPath = explode(EXTENSIONS_PATH . '/', $staticPath);
|
||||
$this->permission_problem = $tmpPath[1] . '/';
|
||||
|
||||
} elseif (file_exists($configFileJson) && !is_writable($configFileJson)) {
|
||||
$tmpPath = explode(EXTENSIONS_PATH . '/', $configFileJson);
|
||||
$this->permission_problem = $tmpPath[1];
|
||||
|
||||
} elseif (Minz_Request::isPost()) {
|
||||
$configWordList = html_entity_decode(Minz_Request::paramString('words_list'));
|
||||
|
||||
$this->word_highlighter_conf = $configWordList;
|
||||
$this->enable_in_article = (bool) Minz_Request::paramString('enable-in-article');
|
||||
$this->enable_logs = (bool) Minz_Request::paramString('enable_logs');
|
||||
$this->case_sensitive = (bool) Minz_Request::paramString('case_sensitive');
|
||||
$this->separate_word_search = (bool) Minz_Request::paramString('separate_word_search');
|
||||
|
||||
$configObj = [
|
||||
'enable_in_article' => $this->enable_in_article,
|
||||
'enable_logs' => $this->enable_logs,
|
||||
'case_sensitive' => $this->case_sensitive,
|
||||
'separate_word_search' => $this->separate_word_search,
|
||||
'words' => preg_split("/\r\n|\n|\r/", $configWordList),
|
||||
];
|
||||
$configJson = json_encode($configObj, WordHighlighterExtension::JSON_ENCODE_CONF);
|
||||
file_put_contents(join_path($staticPath, ('config.' . $current_user . '.json')), $configJson . PHP_EOL);
|
||||
file_put_contents(join_path($staticPath, ('config.' . $current_user . '.js')), $this->jsonToJs($configJson) . PHP_EOL);
|
||||
}
|
||||
|
||||
if (file_exists($configFileJson)) {
|
||||
try {
|
||||
$confJson = json_decode(file_get_contents($configFileJson) ?: '', true, 8, JSON_THROW_ON_ERROR);
|
||||
if (json_last_error() !== JSON_ERROR_NONE || !is_array($confJson)) {
|
||||
return;
|
||||
}
|
||||
$this->enable_in_article = (bool) ($confJson['enable_in_article'] ?? false);
|
||||
$this->enable_logs = (bool) ($confJson['enable_logs'] ?? false);
|
||||
$this->case_sensitive = (bool) ($confJson['case_sensitive'] ?? false);
|
||||
$this->separate_word_search = (bool) ($confJson['separate_word_search'] ?? false);
|
||||
$this->word_highlighter_conf = implode("\n", (array) ($confJson['words'] ?? []));
|
||||
|
||||
} catch (Exception $exception) {
|
||||
// probably nothing to do needed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function jsonToJs(string $jsonStr): string
|
||||
{
|
||||
$js = "window.WordHighlighterConf = " .
|
||||
$jsonStr . ";\n" .
|
||||
"window.WordHighlighterConf.enable_logs && console.log('WordHighlighter: loaded user config:', window.WordHighlighterConf);";
|
||||
return $js;
|
||||
}
|
||||
}
|
||||
15
xExtension-WordHighlighter/i18n/en/ext.php
Normal file
15
xExtension-WordHighlighter/i18n/en/ext.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
return array(
|
||||
'word_highlighter' => array(
|
||||
'write_words' => 'Words to highlight',
|
||||
'write_words_more' => '(separated by newline)',
|
||||
'enable_in_article' => 'Enable highlighting also in article',
|
||||
'enable_in_article_more' => '(⚠️ may be slower with a lot of words)',
|
||||
'enable_logs' => 'Enable logs',
|
||||
'case_sensitive' => 'Case sensitive',
|
||||
'separate_word_search' => 'Separate word search',
|
||||
'test_highlighting_word' => 'highlight',
|
||||
'permission_problem' => 'Your config file is not writable, please change the file permissions for %s',
|
||||
),
|
||||
);
|
||||
15
xExtension-WordHighlighter/i18n/fr/ext.php
Normal file
15
xExtension-WordHighlighter/i18n/fr/ext.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
return array(
|
||||
'word_highlighter' => array(
|
||||
'write_words' => 'Mots à surligner',
|
||||
'write_words_more' => '(séparés par une nouvelle ligne)',
|
||||
'enable_in_article' => 'Activer la mise en évidence également dans l’article',
|
||||
'enable_in_article_more' => '(⚠️ peut être plus lent avec beaucoup de mots)',
|
||||
'enable_logs' => 'Activer les journaux',
|
||||
'case_sensitive' => 'Sensible à la casse',
|
||||
'separate_word_search' => 'Recherche de mots séparés',
|
||||
'test_highlighting_word' => 'surligner',
|
||||
'permission_problem' => 'Votre fichier de configuration n’est pas accessible en écriture, veuillez modifier les permissions du fichier %s',
|
||||
),
|
||||
);
|
||||
8
xExtension-WordHighlighter/metadata.json
Normal file
8
xExtension-WordHighlighter/metadata.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "Word highlighter",
|
||||
"author": "Lukas Melega",
|
||||
"description": "Highlight specific words",
|
||||
"version": "0.0.2",
|
||||
"entrypoint": "WordHighlighter",
|
||||
"type": "user"
|
||||
}
|
||||
BIN
xExtension-WordHighlighter/snapshot-dark.png
Normal file
BIN
xExtension-WordHighlighter/snapshot-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 227 KiB |
BIN
xExtension-WordHighlighter/snapshot.png
Normal file
BIN
xExtension-WordHighlighter/snapshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 210 KiB |
2
xExtension-WordHighlighter/static/.gitignore
vendored
Normal file
2
xExtension-WordHighlighter/static/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
config-words.*.js
|
||||
config-words.*.txt
|
||||
13
xExtension-WordHighlighter/static/mark.min.js
vendored
Normal file
13
xExtension-WordHighlighter/static/mark.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
14
xExtension-WordHighlighter/static/style.css
Normal file
14
xExtension-WordHighlighter/static/style.css
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/* WordHighlighter v0.0.2 (FreshRSS Extension) CSS */
|
||||
#stream mark {
|
||||
padding: 2px;
|
||||
padding-right: 0; /* because in case when part of word is highlighted */
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#stream mark.mark-secondary {
|
||||
background-color: rgba(255, 255, 0, 0.3) !important;
|
||||
}
|
||||
|
||||
html[class*="darkMode"] #stream mark.mark-secondary {
|
||||
background-color: rgba(255, 255, 0, 0.5) !important;
|
||||
}
|
||||
89
xExtension-WordHighlighter/static/word-highlighter.js
Normal file
89
xExtension-WordHighlighter/static/word-highlighter.js
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
'use strict';
|
||||
|
||||
/* WordHighlighter v0.0.2 (FreshRSS Extension) */
|
||||
|
||||
function wordHighlighter(c /* console */, Mark, context, OPTIONS) {
|
||||
const markConf = (done, counter) => ({
|
||||
caseSensitive: OPTIONS.case_sensitive || false,
|
||||
separateWordSearch: OPTIONS.separate_word_search || false,
|
||||
ignoreJoiners: OPTIONS.ignore_joiners || false,
|
||||
exclude: [
|
||||
'mark',
|
||||
...(OPTIONS.enable_in_article ? [] : ['article *']),
|
||||
],
|
||||
done: (n) => (counter.value += n) && done(),
|
||||
noMatch: done,
|
||||
});
|
||||
|
||||
const m = new Mark(context);
|
||||
const changePageListener = debounce(200, (x) => {
|
||||
OPTIONS.enable_logs && c.group('WordHighlighter: page change');
|
||||
stopObserving();
|
||||
highlightWords(m, startObserving);
|
||||
});
|
||||
|
||||
const mo = new MutationObserver(changePageListener);
|
||||
mo.observe(context, { subtree: true, childList: true });
|
||||
|
||||
function startObserving() {
|
||||
mo.observe(context, { subtree: true, childList: true });
|
||||
}
|
||||
|
||||
function stopObserving() {
|
||||
mo.disconnect();
|
||||
}
|
||||
|
||||
function highlightWords(m, done) {
|
||||
const start = performance.now();
|
||||
const hCounter = { value: 0 };
|
||||
|
||||
new Promise((resolve) =>
|
||||
m.mark(OPTIONS.words || [], { ...markConf(resolve, hCounter) })
|
||||
)
|
||||
.finally(() => {
|
||||
if (OPTIONS.enable_logs) {
|
||||
c.log(`WordHighlighter: ${hCounter.value} new highlights added in ${performance.now() - start}ms.`);
|
||||
c.groupEnd();
|
||||
}
|
||||
typeof done === 'function' && done();
|
||||
});
|
||||
}
|
||||
|
||||
highlightWords(m);
|
||||
}
|
||||
|
||||
// MAIN:
|
||||
|
||||
(function main() {
|
||||
try {
|
||||
const confName = 'WordHighlighterConf';
|
||||
const OPTIONS = window[confName] || { };
|
||||
const onMainPage = !(new URL(window.location)).searchParams.get('c');
|
||||
if (onMainPage) {
|
||||
console.log('WordHighlighter: script load...');
|
||||
const context = document.querySelector('#stream');
|
||||
wordHighlighter(console, window.Mark || (Error('mark.js library is not loaded ❗️')), context, OPTIONS);
|
||||
console.log('WordHighlighter: script loaded.✅');
|
||||
} else {
|
||||
OPTIONS.enable_logs && console.log('WordHighlighter: ❗️ paused outside of feed page');
|
||||
}
|
||||
return Promise.resolve();
|
||||
} catch (error) {
|
||||
console.error('WordHighlighter: ❌', error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
})();
|
||||
|
||||
// Util functions:
|
||||
|
||||
function debounce(duration, func) {
|
||||
let timeout;
|
||||
return function (...args) {
|
||||
const effect = () => {
|
||||
timeout = null;
|
||||
return func.apply(this, args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(effect, duration);
|
||||
};
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"name": "ShowFeedID",
|
||||
"author": "math-GH",
|
||||
"description": "Show the feed ID",
|
||||
"version": "0.2.1",
|
||||
"description": "Show the ID of feed and category",
|
||||
"version": "0.3.0",
|
||||
"entrypoint": "ShowFeedID",
|
||||
"type": "user"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ if (url.searchParams.get('c') === 'subscription') {
|
|||
|
||||
button.classList.add('btn');
|
||||
button.id = 'showFeedId';
|
||||
button.innerHTML = '<img class="icon" src="../themes/icons/look.svg" /> FeedID';
|
||||
button.innerHTML = '<img class="icon" src="../themes/icons/look.svg" /> Show IDs';
|
||||
div.appendChild(button);
|
||||
|
||||
document.getElementById('showFeedId').addEventListener('click', function (e) {
|
||||
|
|
@ -22,5 +22,17 @@ if (url.searchParams.get('c') === 'subscription') {
|
|||
feedname_elem.innerHTML = feedname_elem.textContent + ' (ID: ' + feedId + ')';
|
||||
}
|
||||
});
|
||||
|
||||
const cats = document.querySelectorAll('div.box > ul.box-content');
|
||||
|
||||
let catId;
|
||||
let catname_elem;
|
||||
cats.forEach(function (cat) {
|
||||
catId = cat.dataset.catId;
|
||||
catname_elem = cat.parentElement.querySelectorAll('div.box-title > h2')[0];
|
||||
if (catname_elem) {
|
||||
catname_elem.innerHTML = catname_elem.textContent + ' (ID: ' + catId + ')';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue