├── .eslintrc.js
├── .github
└── workflows
│ ├── ci.yml
│ ├── publish.yml
│ └── stale.yml
├── .gitignore
├── .mocharc.json
├── .nycrc.json
├── .stylelintrc.json
├── LICENSE
├── README.md
├── __mocks__
├── example.org.xml
├── invalid.xml
├── obsidian.ts
└── wallabag.xml
├── babel.config.js
├── esbuild.config.mjs
├── jest.config.js
├── manifest.json
├── package.json
├── src
├── actions
│ └── Action.ts
├── consts.ts
├── functions.ts
├── l10n
│ ├── README.md
│ ├── locale.ts
│ └── locales
│ │ ├── da.ts
│ │ ├── de.ts
│ │ ├── en.ts
│ │ ├── es.ts
│ │ ├── fr.ts
│ │ ├── it.ts
│ │ ├── pt.ts
│ │ ├── ru.ts
│ │ ├── test.ts
│ │ └── zh.ts
├── main.ts
├── modals
│ ├── ArticleSuggestModal.ts
│ ├── BaseModal.ts
│ ├── CleanupModal.ts
│ ├── FeedModal.ts
│ ├── FilteredFolderModal.ts
│ ├── ImportModal.ts
│ ├── ItemModal.ts
│ ├── MessageModal.ts
│ ├── TagModal.ts
│ └── TextInputPrompt.ts
├── parser
│ ├── opmlExport.ts
│ ├── opmlParser.ts
│ └── rssParser.ts
├── providers
│ ├── Feed.ts
│ ├── FeedProvider.ts
│ ├── Folder.ts
│ ├── Item.ts
│ ├── Providers.ts
│ ├── local
│ │ ├── LocalFeed.ts
│ │ ├── LocalFeedItem.ts
│ │ ├── LocalFeedProvider.ts
│ │ ├── LocalFeedSettings.ts
│ │ └── LocalFolder.ts
│ └── nextcloud
│ │ ├── NextCloudFeed.ts
│ │ ├── NextCloudFeedSettings.ts
│ │ ├── NextCloudFolder.ts
│ │ ├── NextCloudItem.ts
│ │ └── NextcloudFeedProvider.ts
├── settings
│ ├── AdvancedSettings.ts
│ ├── FileCreationSettings.ts
│ ├── FilterSettings.ts
│ ├── FolderSuggestor.ts
│ ├── HotkeySettings.ts
│ ├── MiscSettings.ts
│ ├── ProviderSettings.ts
│ ├── ProviderValidation.ts
│ ├── SettingsSection.ts
│ ├── SettingsTab.ts
│ ├── settings.ts
│ └── suggest.ts
├── stores.ts
├── style
│ └── main.scss
└── view
│ ├── ArraySuggest.ts
│ ├── FeedFolderSuggest.ts
│ ├── FeedView.svelte
│ ├── FolderView.svelte
│ ├── HtmlTooltip.svelte
│ ├── IconComponent.svelte
│ ├── ItemTitle.svelte
│ ├── ItemView.svelte
│ ├── MainView.svelte
│ ├── MarkdownContent.svelte
│ ├── OldFolderView.svelte
│ ├── TopRowButtons.svelte
│ └── ViewLoader.ts
├── test
├── RssParser.test.ts
├── importData.xml
├── locale.test.ts
├── subfolders.xml
├── subscriptions.xml
└── tsconfig.json
├── tsconfig.json
└── versions.json
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parser: "@typescript-eslint/parser",
4 | plugins: ["@typescript-eslint"],
5 | extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
6 | rules: {
7 | "@typescript-eslint/no-unused-vars": [
8 | 2,
9 | { args: "all", argsIgnorePattern: "^_" },
10 | ],
11 | "no-useless-escape": "off",
12 | "@typescript-eslint/no-explicit-any": "off",
13 | "@typescript-eslint/ban-ts-comment": "off",
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [master]
6 | pull_request:
7 | branches: [master]
8 |
9 | jobs:
10 | lint-and-test:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v2
14 | - name: Install modules
15 | run: yarn
16 | - name: Lint
17 | run: yarn run lint
18 | - name: Lint CSS
19 | run: yarn run lint-css
20 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Build plugin
2 |
3 | on:
4 | push:
5 | # Sequence of patterns matched against refs/tags
6 | tags:
7 | - "*" # Push events to matching any tag format, i.e. 1.0, 20.15.10
8 |
9 | env:
10 | PLUGIN_NAME: rss-reader
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - uses: actions/checkout@v2
18 | - name: Use Node.js
19 | uses: actions/setup-node@v1
20 | with:
21 | node-version: "14.x" # You might need to adjust this value to your own version
22 | - name: Build
23 | id: build
24 | run: |
25 | npm install -g yarn
26 | yarn
27 | yarn run build
28 | mkdir ${{ env.PLUGIN_NAME }}
29 | cp main.js manifest.json ${{ env.PLUGIN_NAME }}
30 | zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }}
31 | ls
32 | echo "::set-output name=tag_name::$(git tag --sort version:refname | tail -n 1)"
33 | - name: Create Release
34 | id: create_release
35 | uses: actions/create-release@v1
36 | env:
37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
38 | VERSION: ${{ github.ref }}
39 | with:
40 | tag_name: ${{ github.ref }}
41 | release_name: ${{ github.ref }}
42 | draft: false
43 | prerelease: false
44 | - name: Upload zip file
45 | id: upload-zip
46 | uses: actions/upload-release-asset@v1
47 | env:
48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
49 | with:
50 | upload_url: ${{ steps.create_release.outputs.upload_url }}
51 | asset_path: ./${{ env.PLUGIN_NAME }}.zip
52 | asset_name: ${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip
53 | asset_content_type: application/zip
54 | - name: Upload main.js
55 | id: upload-main
56 | uses: actions/upload-release-asset@v1
57 | env:
58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
59 | with:
60 | upload_url: ${{ steps.create_release.outputs.upload_url }}
61 | asset_path: ./main.js
62 | asset_name: main.js
63 | asset_content_type: text/javascript
64 | - name: Upload manifest.json
65 | id: upload-manifest
66 | uses: actions/upload-release-asset@v1
67 | env:
68 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
69 | with:
70 | upload_url: ${{ steps.create_release.outputs.upload_url }}
71 | asset_path: ./manifest.json
72 | asset_name: manifest.json
73 | asset_content_type: application/json
74 | - name: Upload main.scss
75 | id: upload-styles
76 | uses: actions/upload-release-asset@v1
77 | env:
78 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
79 | with:
80 | upload_url: ${{ steps.create_release.outputs.upload_url }}
81 | asset_path: ./main.scss
82 | asset_name: main.scss
83 | asset_content_type: text/css
84 |
--------------------------------------------------------------------------------
/.github/workflows/stale.yml:
--------------------------------------------------------------------------------
1 | # This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time.
2 | #
3 | # You can adjust the behavior by modifying this file.
4 | # For more information, see:
5 | # https://github.com/actions/stale
6 | name: Mark stale issues and pull requests
7 |
8 | on:
9 | schedule:
10 | - cron: '32 6 * * *'
11 |
12 | jobs:
13 | stale:
14 |
15 | runs-on: ubuntu-latest
16 | permissions:
17 | issues: write
18 | pull-requests: write
19 |
20 | steps:
21 | - uses: actions/stale@v3
22 | with:
23 | repo-token: ${{ secrets.GITHUB_TOKEN }}
24 | days-before-issue-stale: 30
25 | days-before-issue-close: 14
26 | stale-issue-label: "stale"
27 | stale-issue-message: "This issue is stale because it has been open for 30 days with no activity."
28 | close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
29 | any-of-issue-labels: "question"
30 | days-before-pr-stale: -1
31 | days-before-pr-close: -1
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Intellij
2 | *.iml
3 | .idea
4 |
5 | # npm
6 | node_modules
7 | package-lock.json
8 |
9 | # build
10 | main.js
11 | *.js.map
12 | build/
13 |
14 | # obsidian
15 | data.json
16 | .nyc_output
17 | coverage
18 |
--------------------------------------------------------------------------------
/.mocharc.json:
--------------------------------------------------------------------------------
1 | {
2 | "require": [
3 | "./registerTests.js"
4 | ],
5 | "reporter": "dot"
6 | }
7 |
--------------------------------------------------------------------------------
/.nycrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@istanbuljs/nyc-config-typescript",
3 | "include": [
4 | "src/**/*.ts"
5 | ],
6 | "reporter": [
7 | "text-summary",
8 | "html",
9 | "lcov",
10 | "cobertura"
11 | ],
12 | "report-dir": "./coverage",
13 | "all": true
14 | }
15 |
--------------------------------------------------------------------------------
/.stylelintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["stylelint-config-recommended"],
3 | "rules": {
4 | "font-family-no-missing-generic-family-keyword": null,
5 | "no-descending-specificity": null
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## RSS Reader
2 | Plugin for [Obsidian](https://obsidian.md)
3 |
4 | 
5 | 
6 | 
7 | [](https://liberamanifesto.com)
8 | ---
9 | 
10 |
11 |
12 | ## Features
13 | - Reading RSS feeds from within obsidian
14 | - Sorting feeds into folders
15 | - staring articles
16 | - creating new notes from articles
17 | - pasting article into current note
18 | - creating custom filters
19 | - tagging articles
20 | - support for audio and video feeds
21 | - reading articles with Text to speech (if the [TTS plugin](https://github.com/joethei/obsidian-tts) is installed)
22 | - multi language support(see [#43](https://github.com/joethei/obsidian-rss/issues/43) for translation instructions)
23 | - and more on the [Roadmap](https://github.com/joethei/obsidian-rss/projects/1)
24 |
25 | 
26 |
27 | ## Getting Started
28 |
29 | After installing the plugin:
30 |
31 | - Go to the plugin configuration and add a feed (under the *Content* section).
32 | - In Obsidian, expand the right hand pane and click the RSS tab.
33 |
34 | ## Finding the RSS feed for a website
35 |
36 | - Search for the RSS logo or a link on the website
37 | - Use an browser addon ([Firefox](https://addons.mozilla.org/en-US/firefox/addon/awesome-rss/), [Chrome based](https://chrome.google.com/webstore/detail/get-rss-feed-url/kfghpdldaipanmkhfpdcjglncmilendn))
38 | - Search the websites sourcecode for `rss`
39 |
40 | ## Tips
41 | - get fulltext content for some truncated RSS feeds with [morss.it](https://morss.it/)
42 | - get feeds from some social media sites with [RSS Box](https://rssbox.herokuapp.com/)
43 | - Filter content from feeds with [SiftRSS](https://siftrss.com/)
44 | - Get an RSS feed for a site that does not support RSS with [RSS-proxy](https://github.com/damoeb/rss-proxy/) or [RSS Hub](https://github.com/DIYgod/RSSHub)
45 |
46 | ## Template variables
47 | - `{{title}}` title of article
48 | - `{{link}}` link to article
49 | - `{{author}}` author of article
50 | - `{{published}}` publishing date, you can also specify a custom date format like this: `{{published:YYYYMMDD}}`
51 | - `{{created}}` date of note creation, you can also specify a custom date format like this: `{{created:YYYYMMDD}}`
52 | - `{{content}}` the actual content
53 | - `{{description}}` short description
54 | - `{{folder}}` the folder the feed is in
55 | - `{{feed}}` the name of the feed
56 | - `{{filename}}` the filename, only available in the new file template
57 | - `{{tags}}` - tags, seperated by comma, you can also specify a seperator like this: `{{tags:;}}`
58 | - `{{#tags}}` - tags with #, seperated by comma, with #, you can also specify a seperator like this: `{{#tags:;}}`
59 | - `{{media}}` link to media
60 | - `{{highlights}}` - list of highlights, you can also specify a custom style, this example creates a [admonition](https://github.com/valentine195/obsidian-admonition) for each highlight:
61 | 
62 |
63 | ## ⚠ Security
64 | - This plugin contacts the servers that host the RSS feeds you have specified.
65 | - RSS feeds can contain arbitrary data, this data will get sanitized before being displayed.
66 | - Many Obsidian plugins use codeblocks to add some functionality. This plugin sanitizes these codeblocks at read/note creation time. This is to block rss feeds from executing arbitrary plugin code.
67 | - Some plugins allow for different kinds of inline syntax's, these are treated individually (Currently only _Dataview_ and _Templater_).
68 |
69 |
70 | ## Styling
71 | If you want to style the plugin differently you can use the following css classes
72 |
73 | - rss-read
74 | - rss-not-read
75 | - rss-filters
76 | - rss-folders
77 | - rss-folder
78 | - rss-feed
79 | - rss-feed-title
80 | - rss-feed-items
81 | - rss-feed-item
82 | - rss-tag
83 | - rss-tooltip
84 | - rss-modal
85 | - rss-title
86 | - rss-subtitle
87 | - rss-content
88 |
89 | For help with styling you can also check out the `#appearance` channel on the [Obsidian Members Group Discord](https://obsidian.md/community)
90 |
91 | ### Installing the plugin
92 | - `Settings > Community plugins > Community Plugins > Browse` and search for `RSS Reader`
93 |
94 |
--------------------------------------------------------------------------------
/__mocks__/example.org.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Example Domain
5 |
6 |
7 |
8 |
9 |
36 |
37 |
38 |
39 |
40 |
Example Domain
41 |
This domain is for use in illustrative examples in documents. You may use this
42 | domain in literature without prior coordination or asking for permission.
43 |
More information...
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/__mocks__/invalid.xml:
--------------------------------------------------------------------------------
1 | <
2 |
--------------------------------------------------------------------------------
/__mocks__/obsidian.ts:
--------------------------------------------------------------------------------
1 | import "isomorphic-fetch";
2 | import "fs";
3 | import * as fs from "fs";
4 | import * as path from "path";
5 |
6 | export interface RequestParam {
7 | url: string;
8 | method?: string;
9 | contentType?: string;
10 | body?: string;
11 | headers?: Record;
12 | }
13 |
14 | export async function request(request: RequestParam) : Promise {
15 | if(!request.url.startsWith("http")) {
16 | const filePath = path.join(__dirname, request.url);
17 | return fs.readFileSync(filePath, 'utf-8');
18 | }
19 |
20 | const result = await fetch(request.url,{
21 | headers: request.headers,
22 | method: request.method,
23 | body: request.body
24 | });
25 | return result.text();
26 | }
27 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['@babel/preset-env']
3 | }
4 |
--------------------------------------------------------------------------------
/esbuild.config.mjs:
--------------------------------------------------------------------------------
1 | import esbuild from "esbuild";
2 | import fs from 'fs';
3 | import process from "process";
4 | import builtins from 'builtin-modules';
5 | import sass from "sass";
6 | import autoprefixer from "autoprefixer";
7 | import postcss from "postcss";
8 | import cssnano from "cssnano";
9 | import defaultPreset from "cssnano-preset-default";
10 | import sveltePlugin from "esbuild-svelte";
11 | import sveltePreprocess from "svelte-preprocess";
12 |
13 | const banner =
14 | `/*
15 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
16 | if you want to view the source, please visit the github repository of this plugin
17 | https://github.com/joethei/obisidian-rss
18 | */
19 | `;
20 |
21 | const prod = (process.argv[2] === 'production');
22 |
23 | const copyMinifiedCSS = {
24 | name: 'minify-css',
25 | setup: (build) => {
26 | build.onEnd(async () => {
27 | const {css} = sass.compile('src/style/main.scss');
28 | let result;
29 | if (prod) {
30 | const content = `${banner}\n${css}`;
31 | const preset = defaultPreset({discardComments: false});
32 | result = await postcss([cssnano({
33 | preset: preset,
34 | plugins: [
35 | autoprefixer,
36 | ]
37 | })]).process(content);
38 | } else {
39 | const content = `${banner}\n${css}`;
40 | result = await postcss([autoprefixer]).process(content);
41 | }
42 |
43 | fs.writeFileSync('build/styles.css', result.css, {encoding: 'utf-8'});
44 | })
45 | }
46 | }
47 |
48 | const copyManifest = {
49 | name: 'copy-manifest',
50 | setup: (build) => {
51 | build.onEnd(() => {
52 | fs.copyFileSync('manifest.json', 'build/manifest.json');
53 | });
54 | },
55 | };
56 |
57 | esbuild.build({
58 | banner: {
59 | js: banner,
60 | },
61 | entryPoints: ['src/main.ts'],
62 | bundle: true,
63 | external: ['obsidian', 'electron', ...builtins],
64 | plugins: [sveltePlugin({
65 | preprocess: sveltePreprocess()
66 | }), copyManifest, copyMinifiedCSS],
67 | format: 'cjs',
68 | watch: !prod,
69 | target: 'es2016',
70 | logLevel: "info",
71 | sourcemap: prod ? false : 'inline',
72 | treeShaking: true,
73 | outfile: 'build/main.js',
74 | }).catch(() => process.exit(1));
75 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | transform: {"\\.ts$": ['ts-jest']},
3 | collectCoverage: true,
4 | testEnvironment: "jsdom",
5 | moduleDirectories: ["node_modules", "src", "test"],
6 | coverageReporters: ["lcov", "text", "teamcity"],
7 | testResultsProcessor: "jest-teamcity-reporter",
8 | testMatch: ["**/test/**/*.test.ts"]
9 | };
10 |
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "rss-reader",
3 | "name": "RSS Reader",
4 | "version": "1.2.2",
5 | "minAppVersion": "0.13.33",
6 | "description": "Read RSS Feeds from within obsidian",
7 | "author": "Johannes Theiner",
8 | "authorUrl": "https://github.com/joethei",
9 | "isDesktopOnly": false
10 | }
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rss-reader",
3 | "version": "1.3.0",
4 | "description": "Read RSS Feeds from inside obsidian",
5 | "main": "main.js",
6 | "scripts": {
7 | "dev": "node esbuild.config.mjs",
8 | "build": "node esbuild.config.mjs production",
9 | "lint": "eslint . --ext .ts",
10 | "lint-css": "stylelint src/style/main.scss",
11 | "test": "jest --coverage",
12 | "test:watch": "jest --watch --coverage"
13 | },
14 | "keywords": [],
15 | "author": "Johannes Theiner",
16 | "license": "GPL-3.0",
17 | "devDependencies": {
18 | "@types/lodash.groupby": "^4.6.6",
19 | "@types/lodash.keyby": "^4.6.6",
20 | "@types/lodash.mergewith": "^4.6.6",
21 | "@types/lodash.sortby": "^4.7.6",
22 | "@types/lodash.values": "^4.3.6",
23 | "@types/mocha": "^9.0.0",
24 | "@types/node": "^14.14.37",
25 | "@typescript-eslint/eslint-plugin": "^4.33.0",
26 | "@typescript-eslint/parser": "^4.33.0",
27 | "builtin-modules": "^3.3.0",
28 | "esbuild": "0.13.15",
29 | "esbuild-svelte": "^0.6.0",
30 | "eslint": "^7.32.0",
31 | "isomorphic-fetch": "3.0.0",
32 | "jest": "27.5.1",
33 | "jest-teamcity-reporter": "0.9.0",
34 | "jsdom": "^19.0.0",
35 | "process": "^0.11.10",
36 | "stylelint": "^14.1.0",
37 | "ts-jest": "27.1.4",
38 | "tslib": "^2.3.1",
39 | "tslint": "^6.1.3",
40 | "typescript": "^4.2.4",
41 | "sass": "1.53.0",
42 | "stylelint-scss": "4.3.0",
43 | "stylelint-config-standard": "26.0.0",
44 | "stylelint-config-standard-scss": "5.0.0",
45 | "postcss": "8.4.14",
46 | "autoprefixer": "10.4.7",
47 | "cssnano": "5.1.12",
48 | "cssnano-preset-default": "5.2.12",
49 | "svelte-preprocess": "^4.9.8"
50 | },
51 | "dependencies": {
52 | "@popperjs/core": "^2.10.2",
53 | "@types/nunjucks": "^3.2.1",
54 | "@vanakat/plugin-api": "0.1.0",
55 | "jsdom-global": "^3.0.2",
56 | "lodash.groupby": "^4.6.0",
57 | "lodash.keyby": "^4.6.0",
58 | "lodash.mergewith": "^4.6.2",
59 | "lodash.sortby": "^4.7.0",
60 | "lodash.values": "^4.3.0",
61 | "nunjucks": "3.2.3",
62 | "obsidian": "0.16.3",
63 | "svelte": "^3.43.1",
64 | "ts-md5": "^1.2.10",
65 | "ts-node": "^10.4.0"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/actions/Action.ts:
--------------------------------------------------------------------------------
1 | import {copy, createNewNote, openInBrowser, pasteToNote} from "../functions";
2 | import RssReaderPlugin from "../main";
3 | import {htmlToMarkdown, Notice} from "obsidian";
4 | import {TagModal} from "../modals/TagModal";
5 | import t from "../l10n/locale";
6 | import {Item} from "../providers/Item";
7 |
8 | export default class Action {
9 |
10 | static CREATE_NOTE = new Action(t("create_note"), "create-new", (plugin, item) : Promise => {
11 | return createNewNote(plugin, item);
12 | });
13 |
14 | static PASTE = new Action(t("paste_to_note"), "paste", (plugin, item) : Promise => {
15 | return pasteToNote(plugin, item);
16 | });
17 |
18 | static COPY = new Action(t("copy_to_clipboard"), "documents", ((_, item) : Promise => {
19 | return copy(htmlToMarkdown(item.body()));
20 | }));
21 |
22 | static OPEN = new Action(t("open_browser"), "open-elsewhere-glyph", ((_, item) : Promise => {
23 | openInBrowser(item);
24 | return Promise.resolve();
25 | }));
26 |
27 | static TAGS = new Action(t("edit_tags"), "tag-glyph", (((plugin, item) => {
28 | const modal = new TagModal(plugin, item.tags());
29 |
30 | modal.onClose = async () => {
31 | item.setTags(modal.tags);
32 | const items = plugin.settings.items;
33 | await plugin.writeFeedContent(() => {
34 | return items;
35 | });
36 | };
37 |
38 | modal.open();
39 | return Promise.resolve();
40 | })));
41 |
42 | static READ = new Action(t("mark_as_read_unread"), "eye", ((async (plugin, item) : Promise => {
43 | if (item.read()) {
44 | item.markRead(false);
45 | new Notice(t("marked_as_unread"));
46 | } else {
47 | item.markRead(true);
48 | new Notice(t("marked_as_read"));
49 | }
50 | /*const items = plugin.settings.items;
51 | await plugin.writeFeedContent(() => {
52 | return items;
53 | });*/
54 | return Promise.resolve();
55 | })));
56 |
57 | static FAVORITE = new Action(t("mark_as_favorite_remove"), "star", ((async (plugin, item) : Promise => {
58 | if (item.starred()) {
59 | item.markStarred(false);
60 | new Notice(t("removed_from_favorites"));
61 | } else {
62 | item.markStarred(true);
63 | new Notice(t("added_to_favorites"));
64 | }
65 | /*const items = plugin.settings.items;
66 | await plugin.writeFeedContent(() => {
67 | return items;
68 | });
69 | */
70 | return Promise.resolve();
71 | })));
72 |
73 | static actions = Array.of(Action.FAVORITE, Action.READ, Action.TAGS, Action.CREATE_NOTE, Action.PASTE, Action.COPY, Action.OPEN);
74 |
75 | readonly name: string;
76 | readonly icon: string;
77 | readonly processor: (plugin: RssReaderPlugin, value: Item) => Promise;
78 |
79 | constructor(name: string, icon: string, processor: (plugin: RssReaderPlugin, item: Item) => Promise) {
80 | this.name = name;
81 | this.icon = icon;
82 | this.processor = processor;
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/consts.ts:
--------------------------------------------------------------------------------
1 | export const VIEW_ID = "RSS_FEED";
2 | export const FILE_NAME_REGEX = /["\/<>:|?]/gm;
3 | export const TAG_REGEX = /([\p{Letter}\p{Emoji_Presentation}\p{Number}\/_-]+)/u;
4 | export const NUMBER_REGEX = /^[0-9]*$/gm;
5 |
6 | //taken from: https://stackoverflow.com/a/43467144/5589264
7 | export function isValidHttpUrl(string: string) : boolean {
8 | let url;
9 |
10 | try {
11 | url = new URL(string);
12 | } catch (_) {
13 | return false;
14 | }
15 |
16 | return url.protocol === "http:" || url.protocol === "https:";
17 | }
18 |
--------------------------------------------------------------------------------
/src/l10n/README.md:
--------------------------------------------------------------------------------
1 | ## Localization
2 |
3 | The plugin has full localization support, and will attempt to load the current Obsidian locale. If one does not exist, it will fall back to English.
4 |
5 | ### Adding a new Locale
6 |
7 | New locales can be added by creating a pull request. Two things should be done in this pull request:
8 |
9 | 1. Create the locale in the `locales` folder by copying the `en.ts` file. This file should be given a name matching the [ISO 639-1](https://www.loc.gov/standards/iso639-2/php/English_list.php) code for that language.
10 | 2. Create the translation by editing the value of each property.
11 | 3. Add the import in `locales.ts`.
12 | 4. Add the language to the `localeMap` variable.
13 |
14 | #### Wildcards
15 |
16 | Some strings in the locale have wildcards in them, such as `%1`. This is used by the plugin to insert dynamic data into the translated string.
17 |
18 | For example:
19 |
20 | `Loading RSS Reader v%1`: The plugin will insert the version number for `%1`.
21 |
22 | This allows control of plural syntaxes or placement of file names in the sentence.
23 |
--------------------------------------------------------------------------------
/src/l10n/locale.ts:
--------------------------------------------------------------------------------
1 | //taken from https://github.com/valentine195/obsidian-leaflet-plugin/blob/master/src/l10n/locale.ts
2 | import en from "./locales/en";
3 | import de from "./locales/de";
4 | import zh from "./locales/zh";
5 | import fr from "./locales/fr";
6 | import pt from "./locales/pt";
7 | import it from "./locales/it";
8 | import es from "./locales/es";
9 | import da from "./locales/da";
10 | import test from "./locales/test";
11 |
12 | /* istanbul ignore next */
13 | const locale = (window.moment) ? window.moment.locale() : "test";
14 |
15 | const localeMap: { [k: string]: Partial } = {
16 | en,
17 | de,
18 | "zh-cn": zh,
19 | fr,
20 | es,
21 | test,
22 | pt,
23 | it,
24 | da
25 | };
26 |
27 | const userLocale = localeMap[locale];
28 |
29 | export default function t(str: keyof typeof en, ...inserts: string[]): string {
30 | let localeStr = (userLocale && userLocale[str]) ?? en[str];
31 |
32 | for (let i = 0; i < inserts.length; i++) {
33 | localeStr = localeStr.replace(`%${i + 1}`, inserts[i]);
34 | }
35 |
36 | return localeStr;
37 | }
38 |
--------------------------------------------------------------------------------
/src/l10n/locales/da.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | //these values are only used in testing, don't overwrite them
3 | testingValue: "",
4 | testingInserts: "",
5 |
6 | RSS_Reader: "RSS Reader",
7 | RSS_Feeds: "RSS Feeds",
8 |
9 | //commands
10 | open: "Åben",
11 | refresh_feeds: "Opdater feeds",
12 | create_all: "Opret alle",
13 |
14 | //folder actions
15 | mark_all_as_read: "Marker samtlige som læst",
16 | add_tags_to_all: "Tilføj tags til samtlige",
17 |
18 | filtered_folders: "Filtrerede mapper",
19 | folders: "Mapper",
20 | folder: "Mappe",
21 | feeds: "Feeds",
22 |
23 | //article actions
24 | create_note: "opret ny note",
25 | paste_to_note: "indsæt til nuværende note",
26 | copy_to_clipboard: "kopier til udklipsholder",
27 | open_browser: "åbn i browser",
28 | edit_tags: "rediger tags",
29 | mark_as_read: "marker som læst",
30 | mark_as_unread: "marker som ulæst",
31 | mark_as_favorite: "marker som favorit",
32 | remove_from_favorites: "fjern fra favoritter",
33 | read_article_tts: "læs artikel med TTS (tekst-til-tale)",
34 | next: "næste",
35 | previous: "forrige",
36 |
37 | mark_as_read_unread: "marker som læst/ulæst",
38 | mark_as_favorite_remove: "marker som/fjern fra favoritter",
39 |
40 | //action notifications
41 | marked_as_read: "markeret som læst",
42 | marked_as_unread: "markeret som ulæst",
43 | removed_from_favorites: "fjernet fra favoritter",
44 | added_to_favorites: "markeret som favorit",
45 |
46 | read: "læst",
47 | unread: "ulæst",
48 | favorites: "Favoritter",
49 | favorite: "Favorit",
50 | tags: "Tags",
51 | tag: "Tag",
52 |
53 | //base modal
54 | save: "Gem",
55 | cancel: "Annuller",
56 | delete: "Slet",
57 | edit: "Rediger",
58 | reset: "gendan standard",
59 | fix_errors: "Ret fejl før du gemmer.",
60 |
61 | add_new: "Tilføj ny",
62 |
63 | //feed settings
64 | add_new_feed: "Tilføj nyt feed",
65 | feed_already_configured: "du har allerede et RSS feed konfigureret med det samme url",
66 | no_folder: "Ingen mappe",
67 |
68 | //feed creation modal
69 | name: "Navn",
70 | name_help: "Hvad vil du gerne have at dette RSS feed skal vises som?",
71 | url_help: "Hvilket URL passer til dette RSS feed?",
72 | folder_help: "Hvilken kategori vil du angive dette RSS feed som?",
73 |
74 | invalid_name: "Venligst angiv et navn",
75 | invalid_url: "URL er ikke gyldigt",
76 | invalid_feed: "RSS feed har ingen elementer",
77 |
78 | //filter types
79 | filter_tags: "Alle artikler med tags",
80 | filter_unread: "Alle ulæste artikler (fra mapper)",
81 | filter_read: "Alle læste artikler (fra mapper)",
82 | filter_favorites: "Favoritter (fra mapper)",
83 |
84 | //sort order
85 | sort_date_newest: 'Udgivelsesdato (ny til gammel)',
86 | sort_date_oldest: 'Udgivelsesdato (gammel til ny)',
87 | sort_alphabet_normal: 'Navn (A til Å)',
88 | sort_alphabet_inverted: 'Navn (Å til A)',
89 | sort: 'Sorter efter',
90 |
91 | //filter creation modal
92 | filter_name_help: 'Hvad vil du have, at dette filter skal vises som?',
93 | filter_type: 'Type',
94 | filter_type_help: 'Filtreringstype',
95 | filter: 'Filter',
96 | filter_help: 'Filtrere ved hjælp af mapper/tags, opdelt efter ,',
97 | only_favorites: 'Vis kun favoritter',
98 | show_read: "Vis læste",
99 | show_unread: "Vis ulæste",
100 | filter_folder_help: "Vis kun artikler fra følgende mapper",
101 | filter_feed_help: "Vis kun artikler fra følgende feeds",
102 | filter_tags_help: "Vis kun artikler med følgende tags",
103 |
104 | from_folders: "fra mapper: ",
105 | from_feeds: "fra RSS feeds: ",
106 | with_tags: "inklusive tags: ",
107 |
108 | no_feed_with_name: "Der er intet RSS feed med dette navn",
109 | invalid_tag: "Dette er ikke et gyldigt tag",
110 |
111 | note_exists: "der findes allerede en note med dette navn",
112 | invalid_filename: "filnavnet er ikke gyldigt",
113 |
114 | specify_name: "Angiv et filnavn",
115 | cannot_contain: "kan ikke indeholde:",
116 | created_note: "Oprettet note fra artikel",
117 | inserted_article: "insat artikel i note",
118 | no_file_active: "ingen fil er aktiv",
119 |
120 |
121 | //settings
122 | settings: "Indstillinger",
123 | file_creation: "Fil oprettelse",
124 | template_new: "skabelon til ny fil",
125 | template_new_help: "Når du opretter en note fra en artikel, bliver dette behandlet.",
126 | template_paste: "indsæt artikelskabelon",
127 | template_paste_help: "Når man indsætter/kopierer en artikel, bliver dette behandlet.",
128 | available_variables: "Tilgængelige variabler:",
129 | file_location: "Default location for new notes",
130 | file_location_help: "Standardplacering for nye noter",
131 | file_location_default: "I standard mappe",
132 | file_location_custom: "I nedenstående mappe",
133 | file_location_folder: "Mappe til at oprette nye artikler i",
134 | file_location_folder_help: "nye artikler vil vises i denne mappe",
135 |
136 | date_format: "Dato format",
137 | syntax_reference: "Syntaksreference",
138 | syntax_looks: "Den valgte syntaks ser således ud: ",
139 |
140 | ask_filename: "Spørg efter filnavn",
141 | ask_filename_help: "Deaktiver for at anvende nedenstående skabelon (ugyldige symboler bliver fjernet)",
142 | refresh_time: "Opdateringstid",
143 | refresh_time_help: "Hvor ofte skal RSS feeds opdateres på, angivet i minutter. Anvend 0 for at deaktivere automatisk opdatering",
144 | specify_positive_number: "angiv venligst et positivt tal",
145 | multi_device_usage: "Anvendelse af flere enheder",
146 | multi_device_usage_help: "Hold status for artikler synkroniseret, når du anvender flere enheder på samme tid\n(Kræver genstart)",
147 |
148 | add_new_filter: "Tilføj ny filtreret mappe",
149 | filter_exists: "der er allerede et filter konfigureret med det navn",
150 | hotkeys: "Genvejstaster",
151 | hotkeys_reading: "når du læser en artikel",
152 | press_key: "tryk på en tast",
153 | customize_hotkey: "tilpas genvejstasten",
154 |
155 | refreshed_feeds: "RSS feeds er opdateret",
156 |
157 | //import modal
158 | import: "Import",
159 | import_opml: "Importere fra OPML",
160 | imported_x_feeds: "Importerede %1 feeds",
161 | choose_file: "Vælg fil",
162 | choose_file_help: "Vælg en fil der skal importeres",
163 | export_opml: "Eksportere som OPML",
164 |
165 | default_filename: "Skabelon til filnavn",
166 | default_filename_help: "Alle variabler fra skabelonen er tilgængelige",
167 |
168 | //cleanup modal
169 | cleanup: "Ryd op i artikler",
170 | cleanup_help: "Fjerner elementer, der passer til kriterierne specificeret nedenfor.",
171 | cleanup_help2: "Bemærk at artikler der stadig eksisterer i RSS feedet, vil vise sig når feedet bliver opdateret igen",
172 | perform_cleanup: "Udfør oprydning",
173 | all: "alle",
174 | from_feed: "fra feed",
175 | older_than: "ældre end X dage",
176 | older_than_help: "hold tom for alle vil blive ignoreret hvis der ikke er nogen udgivelsesdato forbundet med indtastningen",
177 | advanced: "Avanceret",
178 | remove_wrong_feed: "Fjern alle artikler, der er i det forkerte feed",
179 | remove_wrong_feed_help: "Dette kan være sket på grund af en fejl i versionerne før 0.8",
180 | scanning_items: "Gennemgår artikler (%1 / %2)",
181 |
182 | created_export: "Oprettet OPML-fil i din Vault",
183 | add: "Tilføj",
184 | from_archive: "Hent gamle artikler fra archive.org",
185 | reading_archive: "Læser data fra arkiv",
186 | scanning_duplicates: "Scanner for dubletter",
187 | do_not_close: "Luk ikke dette vindue",
188 |
189 | display_style: "Visning",
190 | list: "Liste",
191 | cards: "Kort",
192 |
193 | customize_terms: "Tilpas begreber",
194 | content: "Indhold",
195 | highlight: "Marker",
196 | highlight_remove: "fjern markering",
197 |
198 | filter_folder_ignore_help: "ignorer følgende mapper",
199 | filter_feed_ignore_help: "ignorer følgende feeds",
200 | filter_tags_ignore_help: "ignorer følgende tags",
201 |
202 | loading: "Indlæser",
203 |
204 | //template settings
205 | article_title: "Titel",
206 | article_link: "Link til artikel",
207 | article_author: "Artiklens forfatter",
208 | article_published: "Udgivelsesdato",
209 | article_description: "Kort artikelbeskrivelse",
210 | article_content: "Indhold i artiklen",
211 | article_tags: "Tags opdelt med komma",
212 | article_media: "Link til video/lydfil",
213 | feed_folder: "Feed mappe",
214 | feed_title: "Feed titel",
215 | highlights: "Markeringer",
216 | note_created: "Oprettet",
217 | filename: "Filnavn",
218 |
219 | misc: "Diverse",
220 |
221 | display_media: "Inkluder medier",
222 | base_folder: "Standard mappe",
223 |
224 | provider: "Udbyder"
225 | }
226 |
--------------------------------------------------------------------------------
/src/l10n/locales/de.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | RSS_Reader: "RSS Reader",
3 | RSS_Feeds: "RSS Feeds",
4 |
5 | //commands
6 | open: "Öffnen",
7 | refresh_feeds: "Feeds neu laden",
8 | create_all: "Alle erstellen",
9 |
10 | //folder actions
11 | mark_all_as_read: "Alle als gelesen markieren",
12 | add_tags_to_all: "Tags zu allen hinzufügen",
13 |
14 | filtered_folders: "Gefilterte Ordner",
15 | folders: "Ordner",
16 | folder: "Ordner",
17 | feeds: "Feeds",
18 |
19 | //article actions
20 | create_note: "Neue Notiz erstellen",
21 | paste_to_note: "In aktuelle Notiz einfügen",
22 | copy_to_clipboard: "In die Zwischenablage kopieren",
23 | open_browser: "Im Webbrowser öffnen",
24 | edit_tags: "Tags bearbeiten",
25 | mark_as_read: "Als gelesen markieren",
26 | mark_as_unread: "Als ungelesen markieren",
27 | mark_as_favorite: "As Favorit markieren",
28 | remove_from_favorites: "Aus den Favoriten entfernen",
29 | read_article_tts: "Vorlesen",
30 | next: "nächster",
31 | previous: "vorheriger",
32 |
33 | mark_as_read_unread: "Als gelesen/ungelesen markieren",
34 | mark_as_favorite_remove: "Als Favorit markieren/Aus den Favoriten entfernen",
35 |
36 | //action notifications
37 | marked_as_read: "Als gelesen markiert",
38 | marked_as_unread: "Als ungelesen markiert",
39 | removed_from_favorites: "Von den Favoriten entfernt",
40 | added_to_favorites: "Als Favorit markiert",
41 |
42 | read: "gelesen",
43 | unread: "ungelesen",
44 | favorites: "Favoriten",
45 | favorite: "Favorit",
46 | tags: "Tags",
47 | tag: "Tag",
48 |
49 | //base modal
50 | save: "Speichern",
51 | cancel: "Abbrechen",
52 | delete: "Löschen",
53 | edit: "Bearbeiten",
54 | reset: "zurücksetzen",
55 | fix_errors: "Bitte behebe die Fehler vor dem speichern.",
56 |
57 | add_new: "neu hinzufügen",
58 |
59 | //feed settings
60 | add_new_feed: "neuen Feed hinzufügen",
61 | feed_already_configured: "Es existiert bereits ein Feed mit dieser URL",
62 | no_folder: "Kein Ordner",
63 |
64 | //feed creation modal
65 | name: "Name",
66 | name_help: "Unter welchem Namen soll dieser Feed angezeigt werden?",
67 | url_help: "Wie lautet die URL zu diesem Feed?",
68 | folder_help: "Als was kategorisierst du diesen Feed?",
69 |
70 | invalid_name: "Du must einen gültigen Namen vergeben",
71 | invalid_url: "diese URL ist nicht gültig",
72 | invalid_feed: "Dieser Feed hat keine Einträge",
73 |
74 | //filter types
75 | filter_tags: "Alle Artikel mit Tags",
76 | filter_unread: "Alle ungelesenen Artikel(aus Ordnern)",
77 | filter_read: "Alle gelesenen Artikel(aus Ordnern)",
78 | filter_favorites: "Favoriten(aus Ordnern)",
79 |
80 | //sort order
81 | sort_date_newest: 'Veröffentlichungsdatum (neu - alt)',
82 | sort_date_oldest: 'Veröffentlichungsdatum (alt - neu)',
83 | sort_alphabet_normal: 'Name (A - Z)',
84 | sort_alphabet_inverted: 'Name (Z - A)',
85 | sort: 'Ordnen nach',
86 |
87 | //filter creation modal
88 | filter_name_help: 'Wie soll der Filter angezeigt werden?',
89 | filter_type: 'Typ',
90 | filter_type_help: 'Typ des Filters',
91 | filter: 'Filter',
92 | filter_help: 'Order/Tags die gefiltert werden sollen, getrennt durch ,',
93 | only_favorites: 'Zeige nur Favoriten',
94 | show_read: "Zeige gelesene",
95 | show_unread: "Zeige ungelesene",
96 | filter_folder_help: "Zeige nur Artikel aus den folgenden Ordnern",
97 | filter_feed_help: "Zeige nur Artikel aus den folgenden Feeds",
98 | filter_tags_help: "Zeige nur Artikel mit den folgenden Tags",
99 |
100 | from_folders: "Aus Ordnern: ",
101 | from_feeds: "Aus Feeds: ",
102 | with_tags: "Mit Tags: ",
103 |
104 | no_feed_with_name: "Es existiert kein Feed mit diesem Namen",
105 | invalid_tag: "Dieser Tag ist nicht gültig",
106 |
107 | note_exists: "Es existiert bereits eine Notiz mit diesem Namen",
108 | invalid_filename: "Der Dateiname ist nicht gültig",
109 |
110 | specify_name: "Bitte einen Dateinamen angeben",
111 | cannot_contain: "kann nicht enhalten:",
112 | created_note: "Notiz erstellt",
113 | inserted_article: "in Notiz eingefügt",
114 | no_file_active: "Keine Datei geöffnet",
115 |
116 |
117 | //settings
118 | settings: "Einstellungen",
119 | file_creation: "Dateierstellung",
120 | template_new: "Vorlage für neue Dateien",
121 | template_new_help: "Beim erstellen einer Notiz wird dies verarbeitet.",
122 | template_paste: "Vorlage beim Einfügen in eine Datei",
123 | template_paste_help: "Beim einfügen/in die Zwischenablage kopieren wird dies verarbeitet.",
124 | available_variables: "Mögliche Variablen sind:",
125 | file_location: "Speicherort für neue Notizen",
126 | file_location_help: "Wo sollen neue Notizen gespeichert werden?",
127 | file_location_default: "In Standardordner",
128 | file_location_custom: "Eigenen Ordner festlegen",
129 | file_location_folder: "Ordner für neue Notizen",
130 | file_location_folder_help: "Speichert neue Notizen an diesem Ort",
131 |
132 | date_format: "Datumsformat",
133 | syntax_reference: "Syntax Referenz",
134 | syntax_looks: "So wird es aussehen: ",
135 |
136 | ask_filename: "Nach Dateiname fragen",
137 | ask_filename_help: "Deaktivieren um die Vorlage automatisch anzuwenden(ohne ungültige Zeichen)",
138 | refresh_time: "Aktualisierungsintervall",
139 | refresh_time_help: "Wie häufig soll auf neue Einträge überprüft werden(in Minuten), 0 zu deaktivieren",
140 | specify_positive_number: "Bitte eine positive Zahl angeben",
141 | multi_device_usage: "Mit mehreren Geräten nutzen",
142 | multi_device_usage_help: "Syncronisiere Lesestatus & Tags zwischen mehreren gleichzeitig genutzten Geräten\n(Benötigt einen Neustart der App)",
143 |
144 | add_new_filter: "Neuen gefilterten Ordner erstellen",
145 | filter_exists: "Es exisitiert bereits ein Feed mit diesem Namen",
146 | hotkeys: "Tastenkürzel",
147 | hotkeys_reading: "in der Leseansicht",
148 | press_key: "drücke eine Taste",
149 | customize_hotkey: "dieses Tastenkürzel anpassen",
150 |
151 | refreshed_feeds: "Feeds aktualisiert",
152 |
153 | //import modal
154 | import: "Importieren",
155 | import_opml: "Aus OPML importieren",
156 | imported_x_feeds: "%1 Feeds importiert",
157 | choose_file: "Datei auswählen",
158 | choose_file_help: "Wähle eine Datei aus der importiert werden soll",
159 | export_opml: "Als OPML exportieren",
160 |
161 | default_filename: "Vorlage für Dateinamen",
162 | default_filename_help: "Alle Variablen aus der einfügen Vorlage können verwendet werden",
163 |
164 | //cleanup modal
165 | cleanup: "Artikel aufräumen",
166 | cleanup_help: "Entfernt alle Artikel auf die folgende Kriterien zutreffen",
167 | cleanup_help2: "Alle Artikel die noch im Feed vorhanden sind werden beim nächsten aktualisieren wieder erscheinen",
168 | perform_cleanup: "ausführen",
169 | all: "Alle",
170 | from_feed: "von Feed",
171 | older_than: "älter als X Tage",
172 | older_than_help: "Leerlassen für alle, wird ignoriert wenn Artikel kein Veröffentlichungsdatum hat",
173 | advanced: "Erweitert",
174 | remove_wrong_feed: "Alle Artikel entfernen die im falschen Feed gelandet sind",
175 | remove_wrong_feed_help: "Aufgrund eines Fehlers in Versionen vor 0.8 könnte dies passiert sein",
176 | scanning_items: "Verarbeite Artikel (%1 / %2)",
177 |
178 | created_export: "OPML Export in Vault erstellt",
179 | add: "Hinzufügen",
180 | from_archive: "Alte Artikel von archive.org lesen",
181 | reading_archive: "Daten werden aus Archiv geladen",
182 | scanning_duplicates: "Entferne Duplikate",
183 | do_not_close: "Bitte dieses Fenster nicht schliesen",
184 |
185 | display_style: "Anzeige",
186 | list: "List",
187 | cards: "Karten",
188 |
189 | customize_terms: "Begriffe anpassen",
190 | content: "Inhalt",
191 | highlight: "Markieren",
192 | highlight_remove: "Markierung entfernen",
193 |
194 | filter_folder_ignore_help: "diese Ordner ignorieren",
195 | filter_feed_ignore_help: "diese Feeds ignorieren",
196 | filter_tags_ignore_help: "diese Tags ignorieren",
197 |
198 | loading: "Lädt",
199 |
200 | //template settings
201 | article_title: "Titel",
202 | article_link: "Link zum Artikel",
203 | article_author: "Autor",
204 | article_published: "Veröffentlichungsdatum",
205 | article_description: "Kurze Beschreibung des Artikels",
206 | article_content: "Inhalt des Artikels",
207 | article_tags: "Tags getrennt durch Komma",
208 | article_media: "Link zu Video/Audio Datei",
209 | feed_folder: "Ordner des Feeds",
210 | feed_title: "Feed Titel",
211 | highlights: "Highlights",
212 | note_created: "Erstelldatum der Notiz",
213 | filename: "Dateiname"
214 | }
215 |
--------------------------------------------------------------------------------
/src/l10n/locales/en.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | //these values are only used in testing, don't overwrite them
3 | testingValue: "",
4 | testingInserts: "",
5 |
6 | RSS_Reader: "RSS Reader",
7 | RSS_Feeds: "RSS Feeds",
8 |
9 | //commands
10 | open: "Open",
11 | refresh_feeds: "Refresh feeds",
12 | create_all: "Create all",
13 |
14 | //folder actions
15 | mark_all_as_read: "Mark all as read",
16 | add_tags_to_all: "Add tags to all entries",
17 |
18 | filtered_folders: "Filtered Folders",
19 | folders: "Folders",
20 | folder: "Folder",
21 | feeds: "Feeds",
22 |
23 | //article actions
24 | create_note: "create new note",
25 | paste_to_note: "paste to current note",
26 | copy_to_clipboard: "copy to clipboard",
27 | open_browser: "open in browser",
28 | edit_tags: "edit tags",
29 | mark_as_read: "Mark as read",
30 | mark_as_unread: "Mark as unread",
31 | mark_as_favorite: "mark as favorite",
32 | remove_from_favorites: "remove from favorites",
33 | read_article_tts: "read article with TTS",
34 | next: "next",
35 | previous: "previous",
36 |
37 | mark_as_read_unread: "mark as read/unread",
38 | mark_as_favorite_remove: "mark as favorite/remove from favorites",
39 |
40 | //action notifications
41 | marked_as_read: "marked item as read",
42 | marked_as_unread: "marked item as unread",
43 | removed_from_favorites: "removed item from favorites",
44 | added_to_favorites: "marked item as favorite",
45 |
46 | read: "read",
47 | unread: "unread",
48 | favorites: "Favorites",
49 | favorite: "Favorite",
50 | tags: "Tags",
51 | tag: "Tag",
52 |
53 | //base modal
54 | save: "Save",
55 | cancel: "Cancel",
56 | delete: "Delete",
57 | edit: "Edit",
58 | reset: "restore default",
59 | fix_errors: "Please fix errors before saving.",
60 |
61 | add_new: "Add new",
62 |
63 | //feed settings
64 | add_new_feed: "Add new feed",
65 | feed_already_configured: "you already have a feed configured with that url",
66 | no_folder: "No folder",
67 |
68 | //feed creation modal
69 | name: "Name",
70 | name_help: "What do you want this feed to show up as?",
71 | url_help: "What is the URL to the feed?",
72 | folder_help: "What do you categorize this feed as?",
73 |
74 | invalid_name: "you need to specify a name",
75 | invalid_url: "this url is not valid",
76 | invalid_feed: "This feed does not have any entries",
77 |
78 | //filter types
79 | filter_tags: "All articles with tags",
80 | filter_unread: "All unread articles(from folders)",
81 | filter_read: "All read articles(from folders)",
82 | filter_favorites: "Favorites(from folders)",
83 |
84 | //sort order
85 | sort_date_newest: 'Publication date (new to old)',
86 | sort_date_oldest: 'Publication date (old to new)',
87 | sort_alphabet_normal: 'Name (A to Z)',
88 | sort_alphabet_inverted: 'Name (Z to A)',
89 | sort: 'Order by',
90 |
91 | //filter creation modal
92 | filter_name_help: 'What do you want this filter to show up as?',
93 | filter_type: 'Type',
94 | filter_type_help: 'Type of filter',
95 | filter: 'Filter',
96 | filter_help: 'Folders/Tags to filter on, split by ,',
97 | only_favorites: 'Show only favorites',
98 | show_read: "Show read",
99 | show_unread: "Show unread",
100 | filter_folder_help: "Only show articles from the following folders",
101 | filter_feed_help: "Only show articles from the following feeds",
102 | filter_tags_help: "Only show articles with the following tags",
103 |
104 | from_folders: "from folders: ",
105 | from_feeds: "from feeds: ",
106 | with_tags: "with tags: ",
107 |
108 | no_feed_with_name: "There is no feed with this name",
109 | invalid_tag: "This is not a valid tag",
110 |
111 | note_exists: "there is already a note with that name",
112 | invalid_filename: "that filename is not valid",
113 |
114 | specify_name: "Please specify a filename",
115 | cannot_contain: "cannot contain:",
116 | created_note: "Created note from article",
117 | inserted_article: "inserted article into note",
118 | no_file_active: "no file active",
119 |
120 |
121 | //settings
122 | settings: "Settings",
123 | file_creation: "File creation",
124 | template_new: "new file template",
125 | template_new_help: "When creating a note from an article, this gets processed.",
126 | template_paste: "paste article template",
127 | template_paste_help: "When pasting/copying an article this gets processed.",
128 | available_variables: "Available variables are:",
129 | file_location: "Default location for new notes",
130 | file_location_help: "Where newly created notes are placed",
131 | file_location_default: "In the default folder",
132 | file_location_custom: "In the folder specified below",
133 | file_location_folder: "Folder to create new articles in",
134 | file_location_folder_help: "newly created articles will appear in this folder",
135 |
136 | date_format: "Date format",
137 | syntax_reference: "Syntax Reference",
138 | syntax_looks: "Your current syntax looks like this: ",
139 |
140 | ask_filename: "Ask for filename",
141 | ask_filename_help: "Disable to apply the template below automatically(with invalid symbols removed)",
142 | refresh_time: "Refresh time",
143 | refresh_time_help: "How often should the feeds be refreshed, in minutes, use 0 to disable",
144 | specify_positive_number: "please specify a positive number",
145 | multi_device_usage: "Multi device usage",
146 | multi_device_usage_help: "Keep article status synced when using multiple devices at the same time\n(Requires a restart to become effective)",
147 |
148 | add_new_filter: "Add new filtered folder",
149 | filter_exists: "you already have a filter configured with that name",
150 | hotkeys: "Hotkeys",
151 | hotkeys_reading: "when reading an article",
152 | press_key: "press a key",
153 | customize_hotkey: "customize this hotkey",
154 |
155 | refreshed_feeds: "Feeds refreshed",
156 |
157 | //import modal
158 | import: "Import",
159 | import_opml: "Import from OPML",
160 | imported_x_feeds: "Imported %1 feeds",
161 | choose_file: "Choose file",
162 | choose_file_help: "Choose file to import",
163 | export_opml: "Export as OPML",
164 |
165 | default_filename: "Template for filename",
166 | default_filename_help: "All variables from the paste template are available",
167 |
168 | //cleanup modal
169 | cleanup: "Cleanup articles",
170 | cleanup_help: "Removes entries which fit the criteria specified below.",
171 | cleanup_help2: "Keep in mind that articles that still exist in the feed will reappear on the next refresh",
172 | perform_cleanup: "Perform cleanup",
173 | all: "all",
174 | from_feed: "from feed",
175 | older_than: "older than X Days",
176 | older_than_help: "keep empty for all, will be ignored if there is no publishing date associated with entry",
177 | advanced: "Advanced",
178 | remove_wrong_feed: "Remove all articles that are in the incorrect feed",
179 | remove_wrong_feed_help: "This might have happened due to a bug in versions pre 0.8",
180 | scanning_items: "Scanning Articles (%1 / %2)",
181 |
182 | created_export: "Created OPML file in your Vaults root folder",
183 | add: "Add",
184 | from_archive: "Get old articles from archive.org",
185 | reading_archive: "Reading data from archive",
186 | scanning_duplicates: "Scanning for duplicates",
187 | do_not_close: "Please do not close this window",
188 |
189 | display_style: "Display Style",
190 | list: "List",
191 | cards: "Cards",
192 |
193 | customize_terms: "Customize Terms",
194 | content: "Content",
195 | highlight: "Highlight",
196 | highlight_remove: "remove highlight",
197 |
198 | filter_folder_ignore_help: "ignore the following folders",
199 | filter_feed_ignore_help: "ignore the following feeds",
200 | filter_tags_ignore_help: "ignore the following tags",
201 |
202 | loading: "Loading",
203 |
204 | //template settings
205 | article_title: "Title",
206 | article_link: "Link to article",
207 | article_author: "Author of article",
208 | article_published: "Date published",
209 | article_description: "Short article description",
210 | article_content: "article content",
211 | article_tags: "Tags split by comma",
212 | article_media: "Link to video/audio file",
213 | feed_folder: "Folder of feed",
214 | feed_title: "Title of feed",
215 | highlights: "Highlights",
216 | note_created: "Note creation date",
217 | filename: "Filename",
218 |
219 | misc: "Misc",
220 |
221 | display_media: "Include Media",
222 | base_folder: "Base folder",
223 |
224 | provider: "Provider"
225 | }
226 |
--------------------------------------------------------------------------------
/src/l10n/locales/fr.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | //these values are only used in testing, don't overwrite them
3 | testingValue: "",
4 | testingInserts: "",
5 |
6 | RSS_Reader: "Lecteur RSS",
7 | RSS_Feeds: "Fils RSS",
8 |
9 | //commands
10 | open: "Ouvrir",
11 | refresh_feeds: "Rafraîchir les fils",
12 | create_all: "Créer tous les articles",
13 |
14 | //folder actions
15 | mark_all_as_read: "Marquer tous les articles comme lus",
16 | add_tags_to_all: "Ajouter des mots-clés à tous les articles",
17 |
18 | filtered_folders: "Dossiers filtrés",
19 | folders: "Dossiers",
20 | folder: "Dossier",
21 | feeds: "Fils",
22 |
23 | //article actions
24 | create_note: "créer une nouvelle note",
25 | paste_to_note: "coller dans la note actuelle",
26 | copy_to_clipboard: "coller dans le presse-papier",
27 | open_browser: "ouvrir dans le navigateur",
28 | edit_tags: "éditer les mots-clés",
29 | mark_as_read: "Marqeur comme lu",
30 | mark_as_unread: "Marquer comme non lu",
31 | mark_as_favorite: "marquer comme favori",
32 | remove_from_favorites: "retirer des favoris",
33 | read_article_tts: "lire les articles avec TTS",
34 | next: "prochain",
35 | previous: "précédent",
36 |
37 | mark_as_read_unread: "marquer comme lu/non lu",
38 | mark_as_favorite_remove: "marquer comme favori/retirer des favoris",
39 |
40 | //action notifications
41 | marked_as_read: "article marqué comme lu",
42 | marked_as_unread: "article marqué comme non lu",
43 | removed_from_favorites: "article retiré des favoris",
44 | added_to_favorites: "article marqué comme favori",
45 |
46 | read: "lu",
47 | unread: "non lu",
48 | favorites: "Favoris",
49 | favorite: "Favori",
50 | tags: "Mots-clés",
51 | tag: "Mot-clé",
52 |
53 | //base modal
54 | save: "Sauvegarder",
55 | cancel: "Annuler",
56 | delete: "Supprimer",
57 | edit: "Éditer",
58 | reset: "Restaurer les valeurs par défaut",
59 | fix_errors: "Veuillez corriger les erreurs avant de sauvegarder.",
60 |
61 | add_new: "Ajouter un nouveau",
62 |
63 | //feed settings
64 | add_new_feed: "Ajouter un nouveau fil",
65 | feed_already_configured: "Vous avez déjà un fil configuré avec cette URL",
66 | no_folder: "Aucun dossier",
67 |
68 | //feed creation modal
69 | name: "Nom",
70 | name_help: "Comment voulez-vous que ce fil apparaisse?",
71 | url_help: "Quelle est l'URL du fil?",
72 | folder_help: "Comment catégorisez-vous ce fil?",
73 |
74 | invalid_name: "Vous devez spécifier un nom",
75 | invalid_url: "Cet URL n'est pas valide",
76 | invalid_feed: "Ce fil n'a aucune entrée",
77 |
78 | //filter types
79 | filter_tags: "Tous les articles avec des mots-clés",
80 | filter_unread: "Tous les articles non lus (à partir des dossiers)",
81 | filter_read: "Tous les articles lus (à partir des dossiers)",
82 | filter_favorites: "Favoris (à partir des dossiers)",
83 |
84 | //sort order
85 | sort_date_newest: 'Par date (du plus récent au plus ancien)',
86 | sort_date_oldest: 'Par date (du plus ancien au plus récent)',
87 | sort_alphabet_normal: 'Par ordre alphabétique (de A à Z)',
88 | sort_alphabet_inverted: 'Par ordre alphabétique (de Z à A)',
89 | sort: 'Trier par',
90 |
91 | //filter creation modal
92 | filter_name_help: 'Comment voulez-vous que ce filtre apparaisse?',
93 | filter_type: 'Type',
94 | filter_type_help: 'Type de filtre',
95 | filter: 'Filtre',
96 | filter_help: 'Dossiers/Mots-clés à filtrer, séparés par ,',
97 | only_favorites: 'Montrer seulement les favoris',
98 | show_read: "Montrer les articles lus",
99 | show_unread: "Montrer les articles non lus",
100 | filter_folder_help: "Montrer seulement les articles dans ces dossiers",
101 | filter_feed_help: "Montrer seulement les articles dans ces fils",
102 | filter_tags_help: "Montrer seulement les articles avec ces mots-clés",
103 |
104 | from_folders: "des dossiers: ",
105 | from_feeds: "des fils: ",
106 | with_tags: "avec les mots-clés: ",
107 |
108 | no_feed_with_name: "Il n'y a pas de fil avec ce nom",
109 | invalid_tag: "Ce mot-clé n'est pas valide",
110 |
111 | note_exists: "Cette note existe déjà",
112 | invalid_filename: "Ce nom de fichier n'est pas valide",
113 |
114 | specify_name: "Veuillez spécifier un nom",
115 | cannot_contain: "ne peut pas contenir:",
116 | created_note: "Note créée à partir de l'article",
117 | inserted_article: "Article inséré dans la note",
118 | no_file_active: "Aucun fichier actif",
119 |
120 |
121 | //settings
122 | settings: "Réglages",
123 | file_creation: "Création de fichier",
124 | template_new: "Nouveau modèle de fichier",
125 | template_new_help: "Ceci est effectué en créant une note à partir d'un article",
126 | template_paste: "Coller le modèle d'article",
127 | template_paste_help: "Ceci est effectué en collant un modèle d'article dans une note",
128 | available_variables: "Les varibles disponibles sont:",
129 | file_location: "Emplacements par défaut pour les nouvelles notes",
130 | file_location_help: "Là où sont placées les nouvelles notes",
131 | file_location_default: "Dans le dossier par défaut",
132 | file_location_custom: "Dans le dossier spécifié plus bas",
133 | file_location_folder: "Dossiers dans lesquels créer de novuveaux articles",
134 | file_location_folder_help: "Les nouvelles notes seront créées dans ce dossier",
135 |
136 | date_format: "Format de date",
137 | syntax_reference: "Référence de syntaxe",
138 | syntax_looks: "Votre syntaxe actuelle ressemble à ceci:",
139 |
140 | ask_filename: "Demander le nom de fichier",
141 | ask_filename_help: "Désactiver pour appliquer le modèle automatiquement (avec les symboles invalides retirés)",
142 | refresh_time: "Temps de rafraîchissement",
143 | refresh_time_help: "À quelle fréquence le fil sera rechargé en minutes, utiliser 0 pour désactiver",
144 | specify_positive_number: "Veuillez spécifier un nombre positif",
145 | multi_device_usage: "Utilsation sur plusieurs appareils",
146 | multi_device_usage_help: "Garder le statut des articles synchronisé entre plusieurs appareils\n(Nécessite un redémerrage pour prendre effet)",
147 |
148 | add_new_filter: "Ajouter un nouveau dossier filtré",
149 | filter_exists: "Un filtre avec ce nom existe déjà",
150 | hotkeys: "Raccourcis",
151 | hotkeys_reading: "en lisant un article",
152 | press_key: "Appuyez sur une touche",
153 | customize_hotkey: "Personnaliser ce raccourci",
154 |
155 | refreshed_feeds: "Fils rafraîchis",
156 |
157 | //import modal
158 | import: "Importer",
159 | import_opml: "Importer un fichier OPML",
160 | imported_x_feeds: "%1 fils importés",
161 | choose_file: "Choisir un fichier",
162 | choose_file_help: "Choisir un fichier à importer",
163 | export_opml: "Exporter un fichier OPML",
164 |
165 | default_filename: "Modèle pour le nom de fichier",
166 | default_filename_help: "Toutes les variables du modèle collé sont disponibles",
167 |
168 | //cleanup modal
169 | cleanup: "Nettoyer les articles",
170 | cleanup_help: "Retire les entrées qui correspondent au critère suivant:",
171 | cleanup_help2: "Prenez-note que les articles qui existent toujours dans le fil réparraîtront au prochain rafraîchissement",
172 | perform_cleanup: "Nettoyer",
173 | all: "tout",
174 | from_feed: "à partir du fil",
175 | older_than: "plus vieux que X Jours",
176 | older_than_help: "garde vide pour tous, sera ignoré s'il n'existe pas de date de publication associée à l'article",
177 | advanced: "Avancé",
178 | remove_wrong_feed: "Retirer tous les articles qui sont dans un fil incorrect",
179 | remove_wrong_feed_help: "Ceci peut être arrivé en raison d'un bug dans les version pré 0.8",
180 | scanning_items: "Articles en cours de scannage (%1 / %2)",
181 |
182 | created_export: "Fichier OPML créé dans le dossier source",
183 | add: "Ajouter",
184 | from_archive: "Obtenir des anciens articles à partir de archive.org",
185 | reading_archive: "Lecture de l'archive en cours",
186 | scanning_duplicates: "Scan des doublons en cours",
187 | do_not_close: "Ne pas fermer cette fenêtre",
188 |
189 | display_style: "Style d'affichage",
190 | list: "Liste",
191 | cards: "Cartes",
192 |
193 | customize_terms: "Personnaliser les termes",
194 | content: "Contenu",
195 | highlight: "Surligner",
196 | highlight_remove: "Retirer le surlignement",
197 |
198 | filter_folder_ignore_help: "Ignorer les dossiers suivants",
199 | filter_feed_ignore_help: "Ignorer les fils suivants",
200 | filter_tags_ignore_help: "Ignorer les mots-clés suivants",
201 |
202 | loading: "En cours de hargement",
203 |
204 | //template settings
205 | article_title: "Titre",
206 | article_link: "Lien vers l'article",
207 | article_author: "Auteur de l'article",
208 | article_published: "Date de publication",
209 | article_description: "Courte descrition de l'article",
210 | article_content: "Contenu de l'article",
211 | article_tags: "Mots-clés séparés par des virgules",
212 | article_media: "Lien vers la vidéo/ le fichier audio",
213 | feed_folder: "Dossier du fil",
214 | feed_title: "Titre du fil",
215 | highlights: "Surlignements",
216 | note_created: "Date de création de la note",
217 | filename: "Nom du fichier",
218 |
219 | display_media: "Inclure les médias",
220 | base_folder: "Dossier source",
221 | }
222 |
--------------------------------------------------------------------------------
/src/l10n/locales/pt.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | //these values are only used in testing, don't overwrite them
3 | testingValue: "",
4 | testingInserts: "",
5 |
6 | RSS_Reader: "Leitor RSS",
7 | RSS_Feeds: "Feed RSS",
8 |
9 | //commands
10 | open: "Abrir",
11 | refresh_feeds: "Atualizar feed",
12 | create_all: "Criar todos",
13 |
14 | //folder actions
15 | mark_all_as_read: "Marcar todos como lidos",
16 | add_tags_to_all: "Adicionar tags para todos os artigos",
17 |
18 | filtered_folders: "Pastas filtradas",
19 | folders: "Pastas",
20 | folder: "Pasta",
21 | feeds: "Feeds",
22 |
23 | //article actions
24 | create_note: "criar uma nova nota",
25 | paste_to_note: "colar na nota atual",
26 | copy_to_clipboard: "copiar para área de transferência",
27 | open_browser: "abrir no navegador",
28 | edit_tags: "editar tags",
29 | mark_as_read: "marcar como lido",
30 | mark_as_unread: "marcar como não lido",
31 | mark_as_favorite: "marcar como favorito",
32 | remove_from_favorites: "remover dos favoritos",
33 | read_article_tts: "ler artigo com TTS",
34 | next: "próximo",
35 | previous: "anterior",
36 |
37 | mark_as_read_unread: "marcar como lido/não lido",
38 | mark_as_favorite_remove: "marcar como favorito/remover dos favoritos",
39 |
40 | //action notifications
41 | marked_as_read: "item marcado como lido",
42 | marked_as_unread: "item marcado como não lido",
43 | removed_from_favorites: "item removido dos favoritos",
44 | added_to_favorites: "item marcado como favorito",
45 |
46 | read: "lido",
47 | unread: "não lido",
48 | favorites: "Favoritos",
49 | favorite: "Favorito",
50 | tags: "Tags",
51 | tag: "Tag",
52 |
53 | //base modal
54 | save: "Salvar",
55 | cancel: "Cancelar",
56 | delete: "Remover",
57 | edit: "Editar",
58 | reset: "restaurar padrão",
59 | fix_errors: "Por favor corrija erros antes de salvar",
60 |
61 | add_new: "Adicionar novo",
62 |
63 | //feed settings
64 | add_new_feed: "Adicionar novo feed",
65 | feed_already_configured: "você já possui um feeed configurado com esta URL",
66 | no_folder: "Nenhuma pasta",
67 |
68 | //feed creation modal
69 | name: "Nome",
70 | name_help: "Com qual nome você quer que este feed seja mostrado?",
71 | url_help: "Qual é o URL deste feed?",
72 | folder_help: "Como você categoriza este feed?",
73 |
74 | invalid_name: "você precisa especificar um nome",
75 | invalid_url: "esta url não é válido",
76 | invalid_feed: "Este feed não possui nenhum artigo",
77 |
78 | //filter types
79 | filter_tags: "Todos os artigos com tags",
80 | filter_unread: "Todos os artigos não lidos (de pastas)",
81 | filter_read: "Todos os artigos lidos (de pastas)",
82 | filter_favorites: "Favoritos(de pastas)",
83 |
84 | //sort order
85 | sort_date_newest: 'Data de publicação (novo para antigo)',
86 | sort_date_oldest: 'Data de publicação (antigo para novo)',
87 | sort_alphabet_normal: 'Nome (A à Z)',
88 | sort_alphabet_inverted: 'Nome (Z à A)',
89 | sort: 'Ordenar por',
90 |
91 | //filter creation modal
92 | filter_name_help: 'Com qual nome você quer que este filtro seja mostrado?',
93 | filter_type: 'Tipo',
94 | filter_type_help: 'Tipo de filtro',
95 | filter: 'Filtro',
96 | filter_help: 'Pastas/Tags para que seja filtrado, dividido, ',
97 | only_favorites: 'Mostrar apenas favoritos',
98 | show_read: "Mostrar lidos",
99 | show_unread: "Mostrar não lidos",
100 | filter_folder_help: "Apenas mostrar artigos das seguintes pastas",
101 | filter_feed_help: "Apenas mostrar artigos dos seguintes feeds",
102 | filter_tags_help: "Apenas mostrar artigos com as seguintes tags",
103 |
104 | from_folders: "das pastas: ",
105 | from_feeds: "dos feeds: ",
106 | with_tags: "com as tags: ",
107 |
108 | no_feed_with_name: "Não existe um feed com este nome",
109 | invalid_tag: "Esta não é uma tag válida",
110 |
111 | note_exists: "já existe uma nota com este nome",
112 | invalid_filename: "este nome de arquivo não é válido",
113 |
114 | specify_name: "Por favor especifique um nome de arquivo",
115 | cannot_contain: "Não pode conter:",
116 | created_note: "Nota criada de artigo",
117 | inserted_article: "artigo inserido em nota",
118 | no_file_active: "nenhum arquivo ativo",
119 |
120 |
121 | //settings
122 | settings: "Configurações",
123 | file_creation: "Criação de arquivo",
124 | template_new: "Novo modelo de arquivo",
125 | template_new_help: "Quando é criado uma nota de um artigo, este modelo é processado.",
126 | template_paste: "Colar modelo de artigo",
127 | template_paste_help: "Quando é colado/copiado um artigo, este modelo é processado.",
128 | available_variables: "Variáveis disponíveis são:",
129 | file_location: "Local padrão para novas notas",
130 | file_location_help: "Onde notas recentemente criadas são colocadas",
131 | file_location_default: "Em uma pasta padrão",
132 | file_location_custom: "Na pasta especificada abaixo",
133 | file_location_folder: "Pasta onde será criada novos artigos",
134 | file_location_folder_help: "artigos recentemente criados irão aparecer nesta pasta",
135 |
136 | date_format: "Formato de data",
137 | syntax_reference: "Referência de sintaxe",
138 | syntax_looks: "Sua sintaxe atual é exibida como: ",
139 |
140 | ask_filename: "Perguntar por nome de arquivo",
141 | ask_filename_help: "Desativar para aplicar o modelo abaixo automaticamente (com símbolos inválidos removidos)",
142 | refresh_time: "Tempo de atualização",
143 | refresh_time_help: "Quão frequente os feeds devem ser atualizados, em minutos. Use 0 para desativar",
144 | specify_positive_number: "por favor especifique um número positivo",
145 | multi_device_usage: "Uso em múltiplos dispositivos",
146 | multi_device_usage_help: "Mantenha os status de artigos sincronizados usando múltiplos dispositivos ao mesmo tempo\n (Necessário reiniciar para ser efetivo)",
147 |
148 | add_new_filter: "Adicionar nova pasta filtrada",
149 | filter_exists: "você já possui um filtro configurado com este nome",
150 | hotkeys: "atalho",
151 | hotkeys_reading: "Lendo um artigo",
152 | press_key: "Pressione uma tecla",
153 | customize_hotkey: "Customizar este atalho",
154 |
155 | refreshed_feeds: "Feeds atualizados",
156 |
157 | //import modal
158 | import: "Importar",
159 | import_opml: "Importar de OPML",
160 | imported_x_feeds: "Importado %1 feeds",
161 | choose_file: "Escolha arquivo",
162 | choose_file_help: "Escolha arquivo para importar",
163 | export_opml: "Exportar como OPML",
164 |
165 | default_filename: "Modelo para arquivo",
166 | default_filename_help: "Todas as váriaveis do modelo colado está disponíveis",
167 |
168 | //cleanup modal
169 | cleanup: "Limpeza de artigos",
170 | cleanup_help: "Remova artigso que se encaixem em critério especificado abaixo.",
171 | cleanup_help2: "Tenha em mente que artigos que ainda existam no feed irão reaparecer na próxima atualização",
172 | perform_cleanup: "Realizar limpeza",
173 | all: "todos",
174 | from_feed: "de feed",
175 | older_than: "anteriores à X Days",
176 | older_than_help: "matem vazio para todos, será ignorado se nenhuma data de publicação estiver associado ao artigo",
177 | advanced: "Avançado",
178 | remove_wrong_feed: "Remover todos os artigos que estiverem em feed incorreto",
179 | remove_wrong_feed_help: "Isso pode ter acontecido devido a bug in versões pré 0.8",
180 | scanning_items: "Escaneando artigos (%1 / %2)",
181 |
182 | created_export: "Arquivo OPML criado em sua pasta raiz do Cofre",
183 | add: "Adicionar",
184 | from_archive: "Obtenha artigos antigos do archive.org",
185 | reading_archive: "Lendo dados do arquivo",
186 | scanning_duplicates: "Escaneando por duplicatas",
187 | do_not_close: "Por favor não feche esta janela",
188 |
189 | display_style: "Estilo de visualizaçãoDisplay Style",
190 | list: "Lista",
191 | cards: "Cartões",
192 |
193 | customize_terms: "Customizar Termos",
194 | content: "Conteúdo",
195 | highlight: "Destaque",
196 | highlight_remove: "remover destaque",
197 |
198 | filter_folder_ignore_help: "ignorar as seguintes pastas",
199 | filter_feed_ignore_help: "ignorar os seguintes feeds",
200 | filter_tags_ignore_help: "ignorar as seguintes tags",
201 |
202 | loading: "Carregando",
203 |
204 | //template settings
205 | article_title: "Título",
206 | article_link: "Link para artigo",
207 | article_author: "Autor de artigo",
208 | article_published: "Data publicada",
209 | article_description: "Descrição curta de artigo",
210 | article_content: "conteúdo de artigo",
211 | article_tags: "Tags divididas por vírgula",
212 | article_media: "Link para arquivo de vídeo/áudio",
213 | feed_folder: "Pasta de feed",
214 | feed_title: "Título de feed",
215 | highlights: "Destaques",
216 | note_created: "Data de criação da nota",
217 | filename: "Nome do arquivo",
218 |
219 | misc: "Outros",
220 |
221 | display_media: "Incluir mídia",
222 | base_folder: "Pasta base",
223 |
224 | provider: "Provedor"
225 | }
226 |
--------------------------------------------------------------------------------
/src/l10n/locales/ru.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | //these values are only used in testing, don't overwrite them
3 | testingValue: "",
4 | testingInserts: "",
5 |
6 | RSS_Reader: "RSS Reader",
7 | RSS_Feeds: "RSS Feeds",
8 |
9 | //commands
10 | open: "Открыть",
11 | refresh_feeds: "Обновить каналы",
12 | create_all: "Создать все",
13 |
14 | //folder actions
15 | mark_all_as_read: "Пометить все прочитанным",
16 | add_tags_to_all: "Добавить теги ко всем записям",
17 |
18 | filtered_folders: "Отфильтрованная папка",
19 | folders: "Папки",
20 | folder: "Папка",
21 | feeds: "Записи",
22 |
23 | //article actions
24 | create_note: "создать новую заметку",
25 | paste_to_note: "вставить в текущую заметку",
26 | copy_to_clipboard: "скопировать в буфер обмена",
27 | open_browser: "открыть в браузере",
28 | edit_tags: "изменить теги",
29 | mark_as_read: "Пометить прочитанным",
30 | mark_as_unread: "Пометить непрочитанным",
31 | mark_as_favorite: "пометить избранным",
32 | remove_from_favorites: "удалить из избранного",
33 | read_article_tts: "озвучить с помощью TTS",
34 | next: "следующая",
35 | previous: "предыдущая",
36 |
37 | mark_as_read_unread: "пометить как прочитано/непрочитано",
38 | mark_as_favorite_remove: "отметить как избранное/удалить из избранного",
39 |
40 | //action notifications
41 | marked_as_read: "пометил пункт как прочитанный",
42 | marked_as_unread: "пометил пункт как не прочитанный",
43 | removed_from_favorites: "удалил элемент из избранного",
44 | added_to_favorites: "пометил элемент как избранное",
45 |
46 | read: "прочитано",
47 | unread: "не прочитано",
48 | favorites: "Избранное",
49 | favorite: "Избранное",
50 | tags: "Теги",
51 | tag: "Тег",
52 |
53 | //base modal
54 | save: "Сохранить",
55 | cancel: "Отменить",
56 | delete: "Удалить",
57 | edit: "Редактировать",
58 | reset: "востановить по умолчанию",
59 | fix_errors: "Пожайлуста исправьте ошибки перед сохрананением.",
60 |
61 | add_new: "Добавить новую",
62 |
63 | //feed settings
64 | add_new_feed: "Добавить новую ленту",
65 | feed_already_configured: "у вас уже есть канал, настроенный на этот адрес url",
66 | no_folder: "Нет папки",
67 |
68 | //feed creation modal
69 | name: "Имя",
70 | name_help: "Как вы хотите, чтобы эта лента отображалась?",
71 | url_help: "Какой URL-адрес у этой ленты?",
72 | folder_help: "К какой категории вы относите эту ленту?",
73 |
74 | invalid_name: "необходимо указать имя",
75 | invalid_url: "Этот адрес(URL) недействителен",
76 | invalid_feed: "В этой ленте нет записей",
77 |
78 | //filter types
79 | filter_tags: "Все статьи с тегами",
80 | filter_unread: "Все непрочитанные статьи (из папок)",
81 | filter_read: "Все прочитанные статьи (из папок)",
82 | filter_favorites: "Избранное (из папок)",
83 |
84 | //sort order
85 | sort_date_newest: 'Дата публикации (от новой до старой)',
86 | sort_date_oldest: 'Дата публикации (от старой до новой)',
87 | sort_alphabet_normal: 'Имя (от А до Я)',
88 | sort_alphabet_inverted: 'Имя (от Я до А)',
89 | sort: 'Порядок',
90 |
91 | //filter creation modal
92 | filter_name_help: 'Как вы хотите, чтобы этот фильтр отображался?',
93 | filter_type: 'Тип',
94 | filter_type_help: 'Тип фильтра',
95 | filter: 'Фильтр',
96 | filter_help: 'Папки/теги для фильтрации, разделения по ,',
97 | only_favorites: 'Показывать только избранное',
98 | show_read: "Показать прочитанное",
99 | show_unread: "Показать не прочитанное",
100 | filter_folder_help: "Показывайте только статьи из следующих папок",
101 | filter_feed_help: "Показывайте только статьи из следующих лент",
102 | filter_tags_help: "Показывайте только статьи из следующих тег",
103 |
104 | from_folders: "из папки: ",
105 | from_feeds: "из ленты: ",
106 | with_tags: "с тегом: ",
107 |
108 | no_feed_with_name: "Не существует ленты с таким названием",
109 | invalid_tag: "Это недопустимый тег",
110 |
111 | note_exists: "уже есть заметка с таким названием",
112 | invalid_filename: "имя файла недействительно",
113 |
114 | specify_name: "Пожалуйста, укажите имя файла",
115 | cannot_contain: "не может содержать:",
116 | created_note: "Созданная заметка из статьи",
117 | inserted_article: "вставил статью в заметку",
118 | no_file_active: "файл не активен",
119 |
120 |
121 | //settings
122 | settings: "Настройки",
123 | file_creation: "Файл создан",
124 | template_new: "новый шаблон файла",
125 | template_new_help: "При создании заметки из статьи это обрабатывается.",
126 | template_paste: "вставить шаблон статьи",
127 | template_paste_help: "При вставке/копировании статьи это обрабатывается.",
128 | available_variables: "Доступны следующие переменные:",
129 | file_location: "Место по умолчанию для новых заметок",
130 | file_location_help: "Куда помещаются вновь созданные заметки",
131 | file_location_default: "В папке по умолчанию",
132 | file_location_custom: "В папке, указанной ниже",
133 | file_location_folder: "Папка для создания новых статей",
134 | file_location_folder_help: "Вновь созданные статьи будут появляться в этой папке",
135 |
136 | date_format: "Формат даты",
137 | syntax_reference: "Справочник по синтаксису",
138 | syntax_looks: "Ваш текущий синтаксис выглядит следующим образом: ",
139 |
140 | ask_filename: "Задайте имя файла",
141 | ask_filename_help: "Отключите автоматическое применение приведенного ниже шаблона (с удалением недопустимых символов)",
142 | refresh_time: "Время обновления",
143 | refresh_time_help: "Как часто должна обновляться лента, в минутах, используйте 0, чтобы отключить.",
144 | specify_positive_number: "пожалуйста, укажите положительное число",
145 | multi_device_usage: "Использование нескольких устройств",
146 | multi_device_usage_help: "Сохраняйте статус статьи синхронизированным при одновременном использовании нескольких устройств\n(Требуется перезагрузка для вступления в силу)",
147 |
148 | add_new_filter: "Добавление новой отфильтрованной папки",
149 | filter_exists: "у вас уже есть фильтр с таким именем",
150 | hotkeys: "Горячие клавиши",
151 | hotkeys_reading: "при чтении статьи",
152 | press_key: "нажать клавишу",
153 | customize_hotkey: "настройка этой горячей клавиши",
154 |
155 | refreshed_feeds: "Обновление каналов",
156 |
157 | //import modal
158 | import: "Импорт",
159 | import_opml: "Импорт из OPML",
160 | imported_x_feeds: "Импортирована %1 лента",
161 | choose_file: "Выбереите файл",
162 | choose_file_help: "Выберите файл для импорта",
163 | export_opml: "Экспортировать как OPML",
164 |
165 | default_filename: "Шаблон для файлов",
166 | default_filename_help: "Все переменные из шаблона вставки доступны",
167 |
168 | //cleanup modal
169 | cleanup: "Очистка статей",
170 | cleanup_help: "Удаляет записи, соответствующие критериям, указанным ниже.",
171 | cleanup_help2: "Имейте в виду, что статьи, которые все еще существуют в ленте, появятся при следующем обновлении.",
172 | perform_cleanup: "Выполните очистку",
173 | all: "все",
174 | from_feed: "из ленты",
175 | older_than: "старше чем X Дней",
176 | older_than_help: "оставить пустым для всех, будет игнорироваться, если нет даты публикации, связанной с записью",
177 | advanced: "Расширенный",
178 | remove_wrong_feed: "Удалите все статьи, которые находятся в неправильной ленте",
179 | remove_wrong_feed_help: "Это могло произойти из-за ошибки в версиях до 0.8.",
180 | scanning_items: "Сканирование статей (%1 / %2)",
181 |
182 | created_export: "Созданный файл OPML в корневой папке хранилища",
183 | add: "Добавить",
184 | from_archive: "Получайте старые статьи с сайта archive.org",
185 | reading_archive: "Чтение данных из архива",
186 | scanning_duplicates: "Сканирование для поиска дубликатов",
187 | do_not_close: "Пожалуйста, не закрывайте это окно",
188 |
189 | display_style: "Стиль отображения",
190 | list: "Список",
191 | cards: "Карточки",
192 |
193 | customize_terms: "Персонализация условий",
194 | content: "Контент",
195 | highlight: "Выделение",
196 | highlight_remove: "убрать выделение",
197 |
198 | filter_folder_ignore_help: "не обращайте внимания на следующие папки",
199 | filter_feed_ignore_help: "не обращайте внимания на следующие ленты",
200 | filter_tags_ignore_help: "не обращайте внимания на следующие теги",
201 |
202 | loading: "Загрузка",
203 |
204 | //template settings
205 | article_title: "Заголовок",
206 | article_link: "Ссылка на заголовок",
207 | article_author: "Автор заголовка",
208 | article_published: "Дата публикации",
209 | article_description: "Краткое описание статьи",
210 | article_content: "содержание статьи",
211 | article_tags: "Теги, разделенные запятой",
212 | article_media: "Ссылка на видео/аудиофайл",
213 | feed_folder: "Папка с лентами",
214 | feed_title: "Заголовок ленты",
215 | highlights: "Выделение",
216 | note_created: "Дата создания заметки",
217 | filename: "Имя файла",
218 |
219 | misc: "Разное",
220 |
221 | display_media: "Включить медиа",
222 | base_folder: "Базовая папка",
223 |
224 | provider: "Поставщик"
225 | }
226 |
--------------------------------------------------------------------------------
/src/l10n/locales/test.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | "testingValue": "Hello World",
3 | testingInserts: "Hello %1 %2",
4 | }
5 |
--------------------------------------------------------------------------------
/src/l10n/locales/zh.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | RSS_Reader: "RSS Reader",
3 | RSS_Feeds: "订阅源",
4 |
5 | //commands
6 | open: "打开",
7 | refresh_feeds: "更新订阅",
8 | create_all: "创建全部",
9 |
10 | //folder actions
11 | mark_all_as_read: "全部标记为已读",
12 | add_tags_to_all: "为所有条目添加标签",
13 |
14 | filtered_folders: "筛选分类",
15 | folders: "分类",
16 | folder: "分类",
17 | feeds: "订阅源",
18 |
19 | //article actions
20 | create_note: "新建笔记",
21 | paste_to_note: "粘贴到当前笔记",
22 | copy_to_clipboard: "复制到剪切板",
23 | open_browser: "用浏览器打开",
24 | edit_tags: "编辑标签",
25 | mark_as_read: "标记为已读",
26 | mark_as_unread: "标记为未读",
27 | mark_as_favorite: "添加到收藏夹",
28 | remove_from_favorites: "从收藏夹中删除",
29 | read_article_tts: "语音(TTS)阅读文章",
30 | next: "下一篇",
31 | previous: "上一篇",
32 |
33 | mark_as_read_unread: "标记为已读/未读",
34 | mark_as_favorite_remove: "添加到收藏夹/从收藏夹中删除",
35 |
36 | //action notifications
37 | marked_as_read: "已标记为已读",
38 | marked_as_unread: "已标记为未读",
39 | removed_from_favorites: "已从收藏夹中删除",
40 | added_to_favorites: "已添加到收藏夹",
41 |
42 | read: "已读",
43 | unread: "未读",
44 | favorites: "收藏夹",
45 | favorite: "收藏",
46 | tags: "标签",
47 | tag: "标签",
48 |
49 | //base modal
50 | save: "保存",
51 | cancel: "取消",
52 | delete: "删除",
53 | edit: "编辑",
54 | reset: "恢复默认值",
55 | fix_errors: "请在保存前修复错误。",
56 |
57 | add_new: "添加",
58 |
59 | //feed settings
60 | add_new_feed: "添加新订阅源",
61 | feed_already_configured: "您已经添加了该 URL 地址的订阅源",
62 | no_folder: "未分类",
63 |
64 | //feed creation modal
65 | name: "名称",
66 | name_help: "设置订阅源名称",
67 | url_help: "输入订阅源的 URL 地址",
68 | folder_help: "设置订阅源分类",
69 |
70 | invalid_name: "请输入订阅源名称",
71 | invalid_url: "请输入有效的订阅源 URL 地址",
72 | invalid_feed: "此订阅源没有任何内容",
73 |
74 | //filter types
75 | filter_tags: "已打标签的文章",
76 | filter_unread: "全部未读文章(来自分类)",
77 | filter_read: "全部已读文章(来自分类)",
78 | filter_favorites: "收藏夹(来自分类)",
79 |
80 | //sort order
81 | sort_date_newest: '发布日期 (新 → 旧)',
82 | sort_date_oldest: '发布日期 (旧 to 新)',
83 | sort_alphabet_normal: '名称 (A → Z)',
84 | sort_alphabet_inverted: '名称 (Z → A)',
85 | sort: '排序',
86 |
87 | //filter creation modal
88 | filter_name_help: '设置筛选器名称',
89 | filter_type: '类型',
90 | filter_type_help: '筛选器类型',
91 | filter: '筛选器',
92 | filter_help: '要筛选的分类/标签,',
93 | only_favorites: '仅显示已收藏',
94 | show_read: "显示已读",
95 | show_unread: "显示未读",
96 | filter_folder_help: "仅显示以下分类中的文章",
97 | filter_feed_help: "仅显示以下订阅源中的文章",
98 | filter_tags_help: "仅显示以下标签中的文章",
99 |
100 | from_folders: "来自分类: ",
101 | from_feeds: "来自订阅源: ",
102 | with_tags: "来自标签: ",
103 |
104 | no_feed_with_name: "没有找到该名称订阅源",
105 | invalid_tag: "此标签无效",
106 |
107 | note_exists: "已存在同名笔记",
108 | invalid_filename: "文件名无效",
109 |
110 | specify_name: "请输入文件名",
111 | cannot_contain: "不能包含: ",
112 | created_note: "已将该文章复制为笔记",
113 | inserted_article: "已将该文章复制到当前笔记",
114 | no_file_active: "没有文件处于活动状态",
115 |
116 |
117 | //settings
118 | settings: "设置",
119 | file_creation: "新建笔记",
120 | template_new: "笔记模板",
121 | template_new_help: "使用订阅文章创建笔记时,会根据已设置的模板变量进行处理。",
122 | template_paste: "复制/粘贴模板",
123 | template_paste_help: "将订阅文章复制/粘贴为笔记时,会根据已设置的模板变量进行处理。",
124 | available_variables: "可用模板变量: ",
125 | file_location: "保存位置",
126 | file_location_help: "请选择要保存新建笔记的位置",
127 | file_location_default: "默认目录",
128 | file_location_custom: "自定义目录",
129 | file_location_folder: "请选择要保存新建笔记的目录",
130 | file_location_folder_help: "新创建的笔记将保存在该目录中",
131 |
132 | date_format: "日期格式",
133 | syntax_reference: "日期格式语法参考",
134 | syntax_looks: "当前日期格式: ",
135 |
136 | ask_filename: "确认文件名",
137 | ask_filename_help: "禁用则自动使用下面的文件名模板创建文件(自动删除无效的文件名字符)",
138 | refresh_time: "更新频率",
139 | refresh_time_help: "多久更新一次订阅源(单位: 分钟),设置为0则禁用。",
140 | specify_positive_number: "请输入正数",
141 | multi_device_usage: "多设备使用",
142 | multi_device_usage_help: "同时使用多个设备时保持文章状态同步\n(需要重新启动才能生效)",
143 |
144 | add_new_filter: "添加新筛选器",
145 | filter_exists: "已存在同名筛选器",
146 | hotkeys: "快捷键",
147 | hotkeys_reading: "阅读文章时",
148 | press_key: "按下快捷键",
149 | customize_hotkey: "分配快捷键",
150 |
151 | refreshed_feeds: "已更新 RSS 订阅源",
152 |
153 | //import modal
154 | import: "导入",
155 | import_opml: "通过 OPML 导入",
156 | imported_x_feeds: "已导入 %1 条订阅源",
157 | choose_file: "选择文件",
158 | choose_file_help: "请选择要导入的文件",
159 | export_opml: "导出 OPML 文件",
160 |
161 | default_filename: "文件名模板",
162 | default_filename_help: "上面创建笔记的所有模板变量都可用",
163 |
164 | //cleanup modal
165 | cleanup: "清除文章",
166 | cleanup_help: "清除符合以下规则的文章",
167 | cleanup_help2: "注意,订阅源中仍存在的文章将在下次刷新时重新出现",
168 | perform_cleanup: "清除文章",
169 | all: "全部",
170 | from_feed: "来自订阅源",
171 | older_than: "多少天之前发布的文章",
172 | older_than_help: "如果没有符合的文章,则忽略该条规则(为空则保留所有日期的文章)",
173 | advanced: "高级设置",
174 | remove_wrong_feed: "清除所有不正确订阅源中的文章",
175 | remove_wrong_feed_help: "这可能是由于0.8之前版本中的错误造成的",
176 | scanning_items: "扫描文章 (%1 / %2))",
177 |
178 | created_export: "已在笔记仓库根目录创建 OPML 文件",
179 | add: "添加",
180 | from_archive: "从互联网档案馆(archive.org)获取旧文章",
181 | reading_archive: "正在从存档中读取数据",
182 | scanning_duplicates: "扫描重复文章",
183 | do_not_close: "请勿关闭此窗口",
184 |
185 | display_style: "显示风格",
186 | list: "列表",
187 | cards: "卡片",
188 |
189 | customize_terms: "自定义术语",
190 | content: "内容设置",
191 | highlight: "高亮",
192 | highlight_remove: "删除高亮",
193 |
194 | filter_folder_ignore_help: "忽略以下分类",
195 | filter_feed_ignore_help: "忽略以下订阅源",
196 | filter_tags_ignore_help: "忽略以下标签",
197 |
198 | loading: "正在加载",
199 |
200 | //template settings
201 | article_title: "标题",
202 | article_link: "文章链接",
203 | article_author: "文章作者",
204 | article_published: "发布日期",
205 | article_description: "文章摘要",
206 | article_content: "文章正文",
207 | article_tags: "标签以逗号分隔",
208 | article_media: "视频/音频链接",
209 | feed_folder: "订阅源文件夹",
210 | feed_title: "订阅源名称",
211 | highlights: "高亮",
212 | note_created: "笔记创建时间",
213 | filename: "文件名",
214 |
215 | display_media: "包含媒体",
216 | base_folder: "源文件夹"
217 | }
218 |
--------------------------------------------------------------------------------
/src/modals/ArticleSuggestModal.ts:
--------------------------------------------------------------------------------
1 | import {moment, SuggestModal} from "obsidian";
2 | import RssReaderPlugin from "../main";
3 | import {ItemModal} from "./ItemModal";
4 | import {Item} from "../providers/Item";
5 |
6 | export class ArticleSuggestModal extends SuggestModal- {
7 | protected readonly plugin: RssReaderPlugin;
8 | protected readonly items: Item[];
9 |
10 | constructor(plugin: RssReaderPlugin, items: Item[]) {
11 | super(plugin.app);
12 | this.plugin = plugin;
13 | this.items = items;
14 | }
15 |
16 | getItems(): Item[] {
17 | return this.items;
18 | }
19 |
20 | onChooseSuggestion(item: Item, _: MouseEvent | KeyboardEvent): void {
21 | this.close();
22 | new ItemModal(this.plugin, item, this.items, false).open();
23 | }
24 |
25 | getSuggestions(query: string): Item[] {
26 | return this.items.filter((item) => {
27 | return item.title().toLowerCase().includes(query.toLowerCase()) || item.body().toLowerCase().includes(query.toLowerCase());
28 | });
29 | }
30 |
31 | renderSuggestion(item: Item, el: HTMLElement) : void {
32 | el.createEl("div", { text: item.title() });
33 | el.createEl("small", { text: moment(item.pubDate()).format(this.plugin.settings.dateFormat) + " " + item.author() });
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/src/modals/BaseModal.ts:
--------------------------------------------------------------------------------
1 | import {AbstractTextComponent, Modal} from "obsidian";
2 |
3 | export class BaseModal extends Modal {
4 |
5 | //taken from github.com/valentine195/obsidian-admonition
6 | setValidationError(input: AbstractTextComponent, message?: string) : void {
7 | input.inputEl.addClass("is-invalid");
8 | if (message) {
9 | input.inputEl.parentElement.addClasses([
10 | "has-invalid-message",
11 | "unset-align-items"
12 | ]);
13 | input.inputEl.parentElement.addClass(
14 | ".unset-align-items"
15 | );
16 | let mDiv = input.inputEl.parentElement.querySelector(
17 | ".invalid-feedback"
18 | ) as HTMLDivElement;
19 |
20 | if (!mDiv) {
21 | mDiv = createDiv({ cls: "invalid-feedback" });
22 | }
23 | mDiv.innerText = message;
24 | mDiv.insertAfter(input.inputEl);
25 | }
26 | }
27 |
28 | removeValidationError(input: AbstractTextComponent) : void {
29 | input.inputEl.removeClass("is-invalid");
30 | input.inputEl.parentElement.removeClasses([
31 | "has-invalid-message",
32 | "unset-align-items"
33 | ]);
34 | input.inputEl.parentElement.parentElement.removeClass(
35 | ".unset-align-items"
36 | );
37 |
38 | if (
39 | input.inputEl.parentElement.querySelector(".invalid-feedback")
40 | ) {
41 | input.inputEl.parentElement.removeChild(
42 | input.inputEl.parentElement.querySelector(
43 | ".invalid-feedback"
44 | )
45 | );
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/modals/CleanupModal.ts:
--------------------------------------------------------------------------------
1 | import {Notice, Setting, TextComponent, moment} from "obsidian";
2 | import RssReaderPlugin from "../main";
3 | import t from "../l10n/locale";
4 | import {ArraySuggest} from "../view/ArraySuggest";
5 | import {get} from "svelte/store";
6 | import {folderStore} from "../stores";
7 | import {BaseModal} from "./BaseModal";
8 | import {RssFeedContent} from "../parser/rssParser";
9 | import sortBy from "lodash.sortby";
10 | import groupBy from "lodash.groupby";
11 |
12 | export class CleanupModal extends BaseModal {
13 |
14 | plugin: RssReaderPlugin;
15 |
16 | constructor(plugin: RssReaderPlugin) {
17 | super(plugin.app);
18 | this.plugin = plugin;
19 | }
20 |
21 | unread: boolean;
22 | read: boolean;
23 | favorite: boolean;
24 | tag = "";
25 | older_than: number;
26 | feed = "wallabag.xml-option-id";
27 | wrong_feed: boolean;
28 |
29 |
30 | async onOpen() : Promise {
31 | const {contentEl} = this;
32 |
33 | contentEl.empty();
34 |
35 | contentEl.createEl("h1", {text: t("cleanup")});
36 | contentEl.createEl("p", {text: t("cleanup_help")});
37 | contentEl.createEl("p", {text: t("cleanup_help2")});
38 |
39 | new Setting(contentEl)
40 | .setName(t("unread"))
41 | .addToggle(toggle => {
42 | toggle.onChange((value) => {
43 | this.unread = value;
44 | })
45 | });
46 |
47 | new Setting(contentEl)
48 | .setName(t("read"))
49 | .addToggle(toggle => {
50 | toggle.onChange((value) => {
51 | this.read = value;
52 | })
53 | });
54 |
55 | new Setting(contentEl)
56 | .setName(t("favorite"))
57 | .addToggle(toggle => {
58 | toggle.onChange((value) => {
59 | this.favorite = value;
60 | })
61 | });
62 |
63 | new Setting(contentEl)
64 | .setName(t("tag"))
65 | .addSearch(search => {
66 | const tags: string[] = [];
67 | for (const feed of this.plugin.settings.items) {
68 | for(const item of feed.items) {
69 | if(item !== undefined)
70 | tags.push(...item.tags);
71 | }
72 | }
73 | new ArraySuggest(this.app, search.inputEl, new Set(tags));
74 | search
75 | .onChange(async (value: string) => {
76 | this.tag = value;
77 | });
78 | });
79 |
80 | let older_than_setting: TextComponent;
81 | new Setting(contentEl)
82 | .setName(t("older_than"))
83 | .setDesc(t("older_than_help"))
84 | .addText(text => {
85 | older_than_setting = text;
86 | text.setPlaceholder("5")
87 | .onChange(value => {
88 | this.removeValidationError(text);
89 | if (Number(value)) {
90 | this.older_than = Number(value);
91 | }
92 | });
93 |
94 | }).controlEl.addClass("rss-setting-input");
95 | //we don't want decimal numbers.
96 | older_than_setting.inputEl.setAttr("onkeypress", "return event.charCode >= 48 && event.charCode <= 57");
97 |
98 | new Setting(contentEl)
99 | .setName(t("from_feed"))
100 | .addDropdown(dropdown => {
101 | dropdown.addOption("wallabag.xml-option-id", t("all"));
102 |
103 | const sorted = sortBy(groupBy(this.plugin.settings.feeds, "folder"), function (o) {
104 | return o[0].folder;
105 | });
106 | for (const [, feeds] of Object.entries(sorted)) {
107 | for (const id in feeds) {
108 | const feed = feeds[id];
109 | dropdown.addOption(feed.folder + "-" + feed.name, feed.folder + " - " + feed.name);
110 | }
111 | dropdown.setValue(this.feed);
112 | }
113 | dropdown.onChange(value => {
114 | this.feed = value;
115 | });
116 | });
117 |
118 | const details = contentEl.createEl("details");
119 | const summary = details.createEl("summary");
120 | summary.setText(t("advanced"));
121 | const advanced = details.createDiv("advanced");
122 |
123 | new Setting(advanced)
124 | .setName(t("remove_wrong_feed"))
125 | .setDesc(t("remove_wrong_feed_help"))
126 | .addToggle(toggle => {
127 | toggle.onChange((value) => {
128 | this.wrong_feed = value;
129 | })
130 | });
131 |
132 | new Setting(contentEl).addButton((button) => {
133 | button
134 | .setIcon("feather-trash")
135 | .setTooltip(t("perform_cleanup"))
136 | .onClick(async () => {
137 |
138 | let items: RssFeedContent[] = this.plugin.settings.items;
139 |
140 | let date = moment();
141 | if (this.older_than) {
142 | date = moment().subtract(this.older_than, 'days');
143 | }
144 |
145 | let count = 0;
146 | const itemsCount = items.reduce((count, current) => count + current.items.length, 0);
147 | const notice = new Notice(t("scanning_items", "0", itemsCount.toString()));
148 |
149 | for(const feed of items) {
150 | for (const item of feed.items) {
151 | if (item !== undefined) {
152 | let toRemove = 0;
153 | if (item.pubDate === undefined || moment(item.pubDate).isBefore(date)) {
154 | if (this.feed === "wallabag.xml-option-id" || this.feed === (item.folder + "-" + item.feed)) {
155 | if ((this.read && item.read) || (!this.read && !item.read) || (this.read && !item.read)) {
156 | toRemove++;
157 | }
158 | if ((this.unread && !item.read) || (!this.unread && item.read)) {
159 | toRemove++;
160 | }
161 | if ((this.favorite && item.favorite) || (!this.favorite && !item.favorite) || (this.favorite && !item.favorite)) {
162 | toRemove++;
163 | }
164 | if (this.tag === "" || item.tags.includes(this.tag)) {
165 | toRemove++;
166 | }
167 | }
168 |
169 | }
170 | if(toRemove == 4) {
171 | feed.items = feed.items.filter(value => value.hash !== item.hash);
172 | }
173 | }
174 |
175 | count++;
176 | notice.setMessage(t("scanning_items", count.toString(), itemsCount.toString()));
177 | }
178 | }
179 |
180 | if (this.wrong_feed) {
181 | console.log("removing invalid feeds");
182 | const feeds = this.plugin.settings.feeds.map((feed) => {
183 | return feed.name;
184 | });
185 | items = items.filter((item) => {
186 | return feeds.includes(item.name);
187 | });
188 |
189 | const folders = get(folderStore);
190 | items = items.filter((item) => {
191 | return folders.has(item.folder);
192 | });
193 |
194 | //removing wallabag.xml items that do not fit
195 | items.forEach((feed) => {
196 | feed.items = feed.items.filter((item) => {
197 | return feed.name === item.feed && feed.folder === item.folder;
198 | });
199 | });
200 | }
201 |
202 | await this.plugin.writeFeedContent(() => {
203 | return items;
204 | });
205 | this.close();
206 |
207 | });
208 | }).addExtraButton((button) => {
209 | button
210 | .setIcon("cross")
211 | .setTooltip(t("cancel"))
212 | .onClick(() => {
213 | this.close();
214 | })
215 | });
216 | }
217 | }
218 |
--------------------------------------------------------------------------------
/src/modals/FeedModal.ts:
--------------------------------------------------------------------------------
1 | import {Notice, SearchComponent, Setting, TextComponent} from "obsidian";
2 | import RssReaderPlugin from "../main";
3 | import {RssFeed} from "../settings/settings";
4 | import {getFeedItems} from "../parser/rssParser";
5 | import {isValidHttpUrl} from "../consts";
6 | import {BaseModal} from "./BaseModal";
7 | import t from "../l10n/locale";
8 | import {FeedFolderSuggest} from "../view/FeedFolderSuggest";
9 |
10 | export class FeedModal extends BaseModal {
11 | name: string;
12 | url: string;
13 | folder: string;
14 |
15 | saved = false;
16 |
17 | constructor(plugin: RssReaderPlugin, feed?: RssFeed) {
18 | super(plugin.app);
19 |
20 | if(feed) {
21 | this.name = feed.name;
22 | this.url = feed.url;
23 | this.folder = feed.folder;
24 | }
25 | }
26 |
27 | async display() : Promise {
28 | const { contentEl } = this;
29 |
30 | contentEl.empty();
31 |
32 | let nameText: TextComponent;
33 | const name = new Setting(contentEl)
34 | .setName(t("name"))
35 | .setDesc(t("name_help"))
36 | .addText((text) => {
37 | nameText = text;
38 | text.setValue(this.name)
39 | .onChange((value) => {
40 | this.removeValidationError(text);
41 | this.name = value;
42 | });
43 | });
44 | name.controlEl.addClass("rss-setting-input");
45 |
46 | let urlText: TextComponent;
47 | const url = new Setting(contentEl)
48 | .setName("URL")
49 | .setDesc(t("url_help"))
50 | .addText((text) => {
51 | urlText = text;
52 | text.setValue(this.url)
53 | .onChange(async(value) => {
54 | this.removeValidationError(text);
55 | this.url = value;
56 |
57 | });
58 | });
59 | url.controlEl.addClass("rss-setting-input");
60 |
61 | new Setting(contentEl)
62 | .setName(t("folder"))
63 | .setDesc(t("folder_help"))
64 | .addSearch(async (search: SearchComponent) => {
65 | new FeedFolderSuggest(this.app, search.inputEl);
66 | search
67 | .setValue(this.folder)
68 | .setPlaceholder(t("no_folder"))
69 | .onChange(async (value: string) => {
70 | this.folder = value;
71 | });
72 | });
73 |
74 | const footerEl = contentEl.createDiv();
75 | const footerButtons = new Setting(footerEl);
76 | footerButtons.addButton((b) => {
77 | b.setTooltip(t("save"))
78 | .setIcon("checkmark")
79 | .onClick(async () => {
80 | let error = false;
81 | if(!nameText.getValue().length) {
82 | this.setValidationError(nameText, t("invalid_name"));
83 | error = true;
84 | }
85 |
86 | if(!urlText.getValue().length) {
87 | this.setValidationError(urlText, t("invalid_url"));
88 | error = true;
89 | }
90 | if(!isValidHttpUrl(urlText.getValue())) {
91 | this.setValidationError(urlText, t("invalid_url"));
92 | error = true;
93 | }else {
94 | const items = await getFeedItems({name: "test", url: urlText.getValue(), folder: ""});
95 | if(items.items.length == 0) {
96 | this.setValidationError(urlText, t("invalid_feed"));
97 | error = true;
98 | }
99 | }
100 |
101 | if(error) {
102 | new Notice(t("fix_errors"));
103 | return;
104 | }
105 | this.saved = true;
106 | this.close();
107 | });
108 | return b;
109 | });
110 | footerButtons.addExtraButton((b) => {
111 | b.setIcon("cross")
112 | .setTooltip(t("cancel"))
113 | .onClick(() => {
114 | this.saved = false;
115 | this.close();
116 | });
117 | return b;
118 | });
119 | }
120 |
121 | async onOpen() : Promise {
122 | await this.display();
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/src/modals/ImportModal.ts:
--------------------------------------------------------------------------------
1 | import {Modal, Notice, Setting} from "obsidian";
2 | import {loadFeedsFromString} from "../parser/opmlParser";
3 | import RssReaderPlugin from "../main";
4 | import t from "../l10n/locale";
5 | import {FeedFolderSuggest} from "../view/FeedFolderSuggest";
6 |
7 | //adapted from javalent's code here: https://discord.com/channels/686053708261228577/840286264964022302/918146537220112455
8 | export class ImportModal extends Modal {
9 |
10 | plugin: RssReaderPlugin;
11 |
12 | constructor(plugin: RssReaderPlugin) {
13 | super(plugin.app);
14 | this.plugin = plugin;
15 | }
16 |
17 | importData = "";
18 | defaultFolder = "";
19 |
20 | async onOpen() : Promise {
21 | const setting = new Setting(this.contentEl).setName(t("choose_file")).setDesc(t("choose_file_help"));
22 | const input = setting.controlEl.createEl("input", {
23 | attr: {
24 | type: "file",
25 | accept: ".xml,.opml",
26 | }
27 | });
28 |
29 | input.onchange = async () => {
30 | const {files} = input;
31 | if (!files.length) return;
32 | for (const id in files) {
33 | const file = files[id];
34 | const reader = new FileReader();
35 | reader.onload = () => {
36 | this.importData = reader.result as string;
37 | }
38 | reader.readAsText(file);
39 | }
40 | }
41 |
42 | new Setting(this.contentEl)
43 | .setName(t("base_folder"))
44 | .addSearch(search => {
45 | new FeedFolderSuggest(this.app, search.inputEl);
46 | search.setValue(this.defaultFolder)
47 | .onChange(value => {
48 | this.defaultFolder = value;
49 | })
50 | });
51 |
52 | new Setting(this.contentEl).addButton((button) => {
53 | button
54 | .setIcon("import-glyph")
55 | .setTooltip(t("import"))
56 | .onClick(async () => {
57 | if (this.importData) {
58 | const feeds = await loadFeedsFromString(this.importData, this.defaultFolder);
59 | await this.plugin.writeFeeds(() => (this.plugin.settings.feeds.concat(feeds)));
60 | new Notice(t("imported_x_feeds", String(feeds.length)));
61 | this.close();
62 | } else {
63 | new Notice(t("fix_errors"));
64 | }
65 | });
66 | }).addExtraButton((button) => {
67 | button
68 | .setIcon("cross")
69 | .setTooltip(t("cancel"))
70 | .onClick(() => {
71 | this.close();
72 | })
73 | });
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/modals/MessageModal.ts:
--------------------------------------------------------------------------------
1 | import {Modal} from "obsidian";
2 | import RssReaderPlugin from "../main";
3 | import t from "../l10n/locale";
4 |
5 | export class MessageModal extends Modal {
6 |
7 | message: string;
8 |
9 | constructor(plugin: RssReaderPlugin, message: string) {
10 | super(plugin.app);
11 | this.message = message;
12 | }
13 |
14 | onOpen() : void {
15 | this.display();
16 | }
17 |
18 | display() : void {
19 | const {contentEl} = this;
20 |
21 | contentEl.empty();
22 |
23 | contentEl.createEl("h1", {text: this.message});
24 | contentEl.createEl("p", {text: t("do_not_close")});
25 | }
26 |
27 | setMessage(message: string) : void {
28 | this.message = message;
29 | this.display();
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/src/modals/TagModal.ts:
--------------------------------------------------------------------------------
1 | import {BaseModal} from "./BaseModal";
2 | import RssReaderPlugin from "../main";
3 | import {SearchComponent, Setting} from "obsidian";
4 | import {NUMBER_REGEX, TAG_REGEX} from "../consts";
5 | import {ArraySuggest} from "../view/ArraySuggest";
6 | import t from "../l10n/locale";
7 | import {get} from "svelte/store";
8 | import {tagsStore} from "../stores";
9 |
10 | export class TagModal extends BaseModal {
11 | plugin: RssReaderPlugin;
12 | tags: string[];
13 |
14 | constructor(plugin: RssReaderPlugin, tags: string[]) {
15 | super(plugin.app);
16 | this.plugin = plugin;
17 | this.tags = tags;
18 | }
19 |
20 | display(): void {
21 | const {contentEl} = this;
22 | contentEl.empty();
23 |
24 | contentEl.createEl("h1", {text: t("edit_tags")});
25 |
26 | const tagDiv = contentEl.createDiv("tags");
27 |
28 | for (const tag in this.tags) {
29 | new Setting(tagDiv)
30 | .addSearch(async (search: SearchComponent) => {
31 | new ArraySuggest(this.app, search.inputEl, get(tagsStore));
32 | search
33 | .setValue(this.tags[tag])
34 | .onChange(async (value: string) => {
35 | this.removeValidationError(search);
36 | if (!value.match(TAG_REGEX) || value.match(NUMBER_REGEX) || value.contains(" ") || value.contains('#')) {
37 | this.setValidationError(search, t("invalid_tag"));
38 | return;
39 | }
40 | this.tags = this.tags.filter(e => e !== this.tags[tag]);
41 | this.tags.push(value);
42 | });
43 | })
44 | .addExtraButton((button) => {
45 | button
46 | .setTooltip(t("delete"))
47 | .setIcon("trash")
48 | .onClick(() => {
49 | this.tags = this.tags.filter(e => e !== this.tags[tag]);
50 | this.display();
51 | });
52 |
53 | });
54 | }
55 |
56 | let tagValue = "";
57 | let tagComponent: SearchComponent;
58 | const newTag = new Setting(tagDiv)
59 | .addSearch(async (search: SearchComponent) => {
60 | tagComponent = search;
61 | new ArraySuggest(this.app, search.inputEl, get(tagsStore));
62 | search
63 | .onChange(async (value: string) => {
64 | if (!value.match(TAG_REGEX) || value.match(NUMBER_REGEX) || value.contains(" ") || value.contains('#')) {
65 | this.setValidationError(search, t("invalid_tag"));
66 | return;
67 | }
68 | tagValue = value;
69 | });
70 | }).addExtraButton(button => {
71 | button
72 | .setTooltip(t("add"))
73 | .setIcon("plus")
74 | .onClick(() => {
75 | if (!tagValue.match(TAG_REGEX) || tagValue.match(NUMBER_REGEX) || tagValue.contains(" ") || tagValue.contains('#')) {
76 | this.setValidationError(tagComponent, t("invalid_tag"));
77 | return;
78 | }
79 | this.tags.push(tagValue);
80 | this.display();
81 | });
82 | });
83 | newTag.controlEl.addClass("rss-setting-input");
84 |
85 | const buttonEl = contentEl.createSpan("actionButtons");
86 |
87 | new Setting(buttonEl).addExtraButton((btn) =>
88 | btn
89 | .setTooltip(t("save"))
90 | .setIcon("checkmark")
91 | .onClick(async () => {
92 | this.close();
93 | }));
94 | }
95 |
96 | onClose(): void {
97 | const {contentEl} = this;
98 | contentEl.empty();
99 | }
100 |
101 | async onOpen(): Promise {
102 | await this.display();
103 | }
104 |
105 | }
106 |
--------------------------------------------------------------------------------
/src/modals/TextInputPrompt.ts:
--------------------------------------------------------------------------------
1 | import {App, Setting, TextComponent} from "obsidian";
2 | import {BaseModal} from "./BaseModal";
3 | import t from "../l10n/locale";
4 |
5 | //slightly modified version from https://github.com/zsviczian/obsidian-excalidraw-plugin
6 | export class TextInputPrompt extends BaseModal {
7 | private resolve: (value: TextComponent) => void;
8 | private textComponent: TextComponent;
9 | private buttonText: string;
10 |
11 | constructor(app: App, private promptText: string, private hint: string, private defaultValue: string, private placeholder: string, buttonText: string = t("save")) {
12 | super(app);
13 | this.buttonText = buttonText;
14 | }
15 |
16 | onOpen(): void {
17 | this.titleEl.setText(this.promptText);
18 | this.createForm();
19 | }
20 |
21 | onClose(): void {
22 | this.contentEl.empty();
23 | }
24 |
25 | createForm(): void {
26 | const div = this.contentEl.createDiv();
27 |
28 | const text = new Setting(div).setName(this.promptText).setDesc(this.hint).addText((textComponent) => {
29 | textComponent
30 | .setValue(this.defaultValue)
31 | .setPlaceholder(this.placeholder)
32 | .onChange(() => {
33 | this.removeValidationError(textComponent);
34 | })
35 | .inputEl.setAttribute("size", "50");
36 | this.textComponent = textComponent;
37 | });
38 | text.controlEl.addClass("rss-setting-input");
39 |
40 | new Setting(div).addButton((b) => {
41 | b
42 | .setButtonText(this.buttonText)
43 | .onClick(async () => {
44 | this.resolve(this.textComponent);
45 | });
46 | return b;
47 | });
48 | }
49 |
50 | async openAndGetValue(resolve: (value: TextComponent) => void): Promise {
51 | this.resolve = resolve;
52 | await this.open();
53 | }
54 | }
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/src/parser/opmlExport.ts:
--------------------------------------------------------------------------------
1 | import {RssFeed} from "../settings/settings";
2 | import groupBy from "lodash.groupby";
3 |
4 | export function generateOPML(feeds: RssFeed[]) : string {
5 | const doc = document.implementation.createDocument("", "opml");
6 |
7 | const head = doc.createElement("head");
8 | const title = doc.createElement("title");
9 | head.appendChild(title);
10 | title.setText("Obsidian RSS Export");
11 |
12 | doc.documentElement.appendChild(head);
13 | const body = doc.createElement("body");
14 |
15 | doc.documentElement.appendChild(body);
16 |
17 | const sorted = groupBy(feeds, "folder");
18 | for(const id of Object.keys(sorted)) {
19 | const folder = sorted[id];
20 | const outline = doc.createElement("outline");
21 | body.appendChild(outline);
22 | outline.setAttribute("title", folder[0].folder);
23 | for (const feed of folder) {
24 | const exportFeed = doc.createElement("outline");
25 | exportFeed.setAttribute("title", feed.name)
26 | exportFeed.setAttribute("xmlUrl", feed.url);
27 | outline.append(exportFeed);
28 | }
29 | }
30 |
31 | return new XMLSerializer().serializeToString(doc.documentElement);
32 | }
33 |
--------------------------------------------------------------------------------
/src/parser/opmlParser.ts:
--------------------------------------------------------------------------------
1 | import {RssFeed} from "../settings/settings";
2 |
3 | export async function loadFeedsFromString(importData: string, defaultFolder: string): Promise {
4 | const rawData = new window.DOMParser().parseFromString(importData, "text/xml");
5 | const feeds: RssFeed[] = [];
6 |
7 |
8 | const outlines = rawData.getElementsByTagName('outline');
9 |
10 | for (let i = 0, max = outlines.length; i < max; i++) {
11 |
12 | const current = outlines[i];
13 | if (!current.hasChildNodes()) {
14 |
15 | const title = current.getAttribute("title");
16 | const xmlUrl = current.getAttribute('xmlUrl');
17 |
18 | if(current.parentElement.hasAttribute("title")) {
19 | feeds.push({
20 | name: title,
21 | url: xmlUrl,
22 | folder: defaultFolder + ((defaultFolder) ? "/" : "") + current.parentElement.getAttribute("title"),
23 | });
24 | }else {
25 | feeds.push({
26 | name: title,
27 | url: xmlUrl,
28 | folder: defaultFolder + ""
29 | });
30 | }
31 | }
32 | }
33 |
34 | return feeds;
35 | }
36 |
--------------------------------------------------------------------------------
/src/parser/rssParser.ts:
--------------------------------------------------------------------------------
1 | import {request} from "obsidian";
2 | import {RssFeed} from "../settings/settings";
3 | import {Md5} from "ts-md5";
4 |
5 | /**
6 | * parser for .rss files, build from scratch
7 | * because I could not find a parser that
8 | * - works on mobile
9 | * - is up-to-date
10 | * - works for multiple different interpretations of the rss spec
11 | */
12 |
13 | export interface RssFeedContent {
14 | subtitle: string,
15 | title: string,
16 | name: string,
17 | link: string,
18 | image: string,
19 | folder: string,
20 | description: string,
21 | language: string,
22 | hash: string,
23 | items: RssFeedItem[]
24 | }
25 |
26 | export interface RssFeedItem {
27 | title: string,
28 | description: string,
29 | content: string,
30 | category: string,
31 | link: string,
32 | creator: string,
33 | language: string,
34 | enclosure: string,
35 | enclosureType: string,
36 | image: string,
37 | pubDate: string,
38 | folder: string,
39 | feed: string,
40 | favorite: boolean,
41 | read: boolean,
42 | created: boolean,
43 | tags: string[],
44 | hash: string,
45 | id: string,
46 | highlights: string[],
47 | }
48 |
49 | /**
50 | * return the node with the specified name
51 | * : to get namespaced element
52 | * . to get nested element
53 | * @param element
54 | * @param name
55 | */
56 | function getElementByName(element: Element | Document, name: string): ChildNode {
57 | let value: ChildNode;
58 | if (typeof element.getElementsByTagName !== 'function' && typeof element.getElementsByTagNameNS !== 'function') {
59 | //the required methods do not exist on element, aborting
60 | return;
61 | }
62 |
63 | if (name.includes(":")) {
64 | const [namespace, tag] = name.split(":");
65 | const namespaceUri = element.lookupNamespaceURI(namespace);
66 | const byNamespace = element.getElementsByTagNameNS(namespaceUri, tag);
67 | if (byNamespace.length > 0) {
68 | value = byNamespace[0].childNodes[0];
69 | } else {
70 | //there is no element in that namespace, probably because no namespace has been defined
71 | const tmp = element.getElementsByTagName(name);
72 | if (tmp.length > 0) {
73 | if (tmp[0].childNodes.length === 0) {
74 | value = tmp[0];
75 | } else {
76 | const node = tmp[0].childNodes[0];
77 | if (node !== undefined) {
78 | value = node;
79 | }
80 | }
81 | }
82 | }
83 |
84 | } else if (name.includes(".")) {
85 | const [prefix, tag] = name.split(".");
86 | if (element.getElementsByTagName(prefix).length > 0) {
87 | const nodes = Array.from(element.getElementsByTagName(prefix)[0].childNodes);
88 | nodes.forEach((node) => {
89 | if (node.nodeName == tag) {
90 | value = node;
91 | }
92 | });
93 | }
94 |
95 | } else if (element.getElementsByTagName(name).length > 0) {
96 | if (element.getElementsByTagName(name)[0].childNodes.length == 0) {
97 | value = element.getElementsByTagName(name)[0];
98 | } else {
99 | const node = element.getElementsByTagName(name)[0].childNodes[0];
100 | if (node !== undefined)
101 | value = node;
102 | }
103 | }
104 | //if(name === "content") console.log(value);
105 |
106 | return value;
107 | }
108 |
109 | /**
110 | * # to get attribute
111 | * Always returns the last found value for names
112 | * @param element
113 | * @param names possible names
114 | */
115 | function getContent(element: Element | Document, names: string[]): string {
116 | let value: string;
117 | for (const name of names) {
118 | if (name.includes("#")) {
119 | const [elementName, attr] = name.split("#");
120 | const data = getElementByName(element, elementName);
121 | if (data) {
122 | if (data.nodeName === elementName) {
123 | //@ts-ignore
124 | const tmp = data.getAttribute(attr);
125 | if (tmp.length > 0) {
126 | value = tmp;
127 | }
128 | }
129 | }
130 | } else {
131 | const data = getElementByName(element, name);
132 | if (data) {
133 | //@ts-ignore
134 | if(data.wholeText && data.wholeText.length > 0) {
135 | //@ts-ignore
136 | value = data.wholeText;
137 | }
138 |
139 | //@ts-ignore
140 | if (!value && data.nodeValue && data.nodeValue.length > 0) {
141 | value = data.nodeValue;
142 | }
143 | //@ts-ignore
144 | if (!value && data.innerHTML && data.innerHTML.length > 0) {
145 | //@ts-ignore
146 | value = data.innerHTML;
147 | }
148 | }
149 | }
150 | }
151 | if (value === undefined) {
152 | return "";
153 | }
154 | return value;
155 | }
156 |
157 | function buildItem(element: Element): RssFeedItem {
158 | return {
159 | title: getContent(element, ["title"]),
160 | description: getContent(element, ["content", "content:encoded", "itunes:summary", "description", "summary", "media:description"]),
161 | content: getContent(element, ["itunes:summary", "description", "summary", "media:description", "content", "content:encoded", "ns0:encoded"]),
162 | category: getContent(element, ["category"]),
163 | link: getContent(element, ["link", "link#href"]),
164 | creator: getContent(element, ["creator", "dc:creator", "author", "author.name"]),
165 | pubDate: getContent(element, ["pubDate", "published", "updated", "dc:date"]),
166 | enclosure: getContent(element, ["enclosure#url", "yt:videoId"]),
167 | enclosureType: getContent(element, ["enclosure#type"]),
168 | image: getContent(element, ["enclosure#url", "media:content#url", "itunes:image#href", "media:thumbnail#url"]),
169 | id: getContent(element, ["id"]),
170 | language: null,
171 | folder: null,
172 | feed: null,
173 | read: null,
174 | favorite: null,
175 | created: null,
176 | tags: [],
177 | hash: null,
178 | highlights: []
179 | }
180 | }
181 |
182 | function getAllItems(doc: Document): Element[] {
183 | const items: Element[] = [];
184 |
185 | if (doc.getElementsByTagName("item")) {
186 | for (const elementsByTagNameKey in doc.getElementsByTagName("item")) {
187 | const entry = doc.getElementsByTagName("item")[elementsByTagNameKey];
188 | items.push(entry);
189 |
190 | }
191 | }
192 | if (doc.getElementsByTagName("entry")) {
193 | for (const elementsByTagNameKey in doc.getElementsByTagName("entry")) {
194 | const entry = doc.getElementsByTagName("entry")[elementsByTagNameKey];
195 | items.push(entry);
196 | }
197 | }
198 | return items;
199 | }
200 |
201 | async function requestFeed(feed: RssFeed) : Promise {
202 | return await request({url: feed.url});
203 | }
204 |
205 | export async function getFeedItems(feed: RssFeed): Promise {
206 | let data;
207 | try {
208 | const rawData = await requestFeed(feed);
209 | data = new window.DOMParser().parseFromString(rawData, "text/xml");
210 | } catch (e) {
211 | console.error(e);
212 | return Promise.resolve(undefined);
213 | }
214 |
215 |
216 | const items: RssFeedItem[] = [];
217 | const rawItems = getAllItems(data);
218 |
219 | const language = getContent(data, ["language"]).substr(0, 2);
220 |
221 | rawItems.forEach((rawItem) => {
222 | const item = buildItem(rawItem);
223 | if (item.title !== undefined && item.title.length !== 0) {
224 | item.folder = feed.folder;
225 | item.feed = feed.name;
226 | item.read = false;
227 | item.favorite = false;
228 | item.created = false;
229 | item.language = language;
230 | item.hash = new Md5().appendStr(item.title).appendStr(item.folder).appendStr(item.link).end();
231 |
232 | if (!item.image && feed.url.includes("youtube.com/feeds")) {
233 | item.image = "https://i3.ytimg.com/vi/" + item.id.split(":")[2] + "/hqdefault.jpg";
234 | }
235 |
236 | items.push(item);
237 | }
238 | })
239 | const image = getContent(data, ["image", "image.url", "icon"]);
240 |
241 | const content: RssFeedContent = {
242 | title: getContent(data, ["title"]),
243 | subtitle: getContent(data, ["subtitle"]),
244 | link: getContent(data, ["link"]),
245 | //we don't want any leading or trailing slashes in image urls(i.e. reddit does that)
246 | image: image ? image.replace(/^\/|\/$/g, '') : null,
247 | description: getContent(data, ["description"]),
248 | items: items,
249 | folder: feed.folder,
250 | name: feed.name,
251 | language: language,
252 | hash: "",
253 | };
254 |
255 | return Promise.resolve(content);
256 | }
257 |
--------------------------------------------------------------------------------
/src/providers/Feed.ts:
--------------------------------------------------------------------------------
1 | import {Item} from "./Item";
2 |
3 | export enum FeedOrder {
4 | DEFAULT,
5 | OLDEST_FIRST,
6 | NEWEST_FIRST,
7 | }
8 |
9 |
10 | export interface Feed {
11 | id(): number;
12 | url(): string;
13 | title(): string;
14 | favicon(): string;
15 | unreadCount(): number;
16 | ordering(): FeedOrder;
17 | link(): string;
18 | folderId(): number;
19 | folderName(): string;
20 | items(): Item[];
21 | }
22 |
--------------------------------------------------------------------------------
/src/providers/FeedProvider.ts:
--------------------------------------------------------------------------------
1 | import {Feed} from "./Feed";
2 | import {Folder} from "./Folder";
3 | import {Item} from "./Item";
4 | import {SettingsSection} from "../settings/SettingsSection";
5 |
6 | export interface FeedProvider {
7 |
8 | id(): string;
9 |
10 | name(): string;
11 |
12 | isValid(): Promise;
13 |
14 | warnings() : string[];
15 |
16 | folders(): Promise;
17 |
18 | filteredFolders() : Promise;
19 |
20 | feeds(): Promise;
21 |
22 | items() : Promise
- ;
23 |
24 | settings(containerEl: HTMLDivElement) : SettingsSection;
25 | }
26 |
--------------------------------------------------------------------------------
/src/providers/Folder.ts:
--------------------------------------------------------------------------------
1 | import {Feed} from "./Feed";
2 |
3 | export interface Folder {
4 | id(): number;
5 | name(): string;
6 | feeds(): Feed[];
7 | }
8 |
--------------------------------------------------------------------------------
/src/providers/Item.ts:
--------------------------------------------------------------------------------
1 | export interface Item {
2 |
3 | id(): string | number;
4 | guid(): string;
5 | guidHash(): string;
6 | url(): string;
7 | title(): string;
8 | author(): string;
9 | pubDate(): string;
10 | body(): string;
11 | description(): string;
12 | feedId(): number;
13 | read(): boolean;
14 | starred(): boolean;
15 | rtl(): boolean;
16 | mediaThumbnail(): string;
17 | mediaDescription(): string;
18 | enclosureMime(): string;
19 | enclosureLink(): string;
20 | markStarred(starred: boolean): void;
21 | markRead(read: boolean): void;
22 | tags(): string[];
23 | setTags(tags: string[]): void;
24 | created(): boolean;
25 | markCreated(created: boolean): void;
26 | language(): string | undefined;
27 | highlights(): string[];
28 | folder(): string;
29 | feed(): string;
30 | }
31 |
--------------------------------------------------------------------------------
/src/providers/Providers.ts:
--------------------------------------------------------------------------------
1 | import {FeedProvider} from "./FeedProvider";
2 | import RssReaderPlugin from "../main";
3 |
4 | export class Providers {
5 | private plugin: RssReaderPlugin;
6 | private providers: FeedProvider[] = [];
7 |
8 | constructor(plugin: RssReaderPlugin) {
9 | this.plugin = plugin;
10 | }
11 |
12 | getAll(): FeedProvider[] {
13 | return this.providers;
14 | }
15 |
16 | getCurrent(): FeedProvider {
17 | return this.getById(this.plugin.settings.provider);
18 | }
19 |
20 | getById(id: string): FeedProvider {
21 | return this.providers.filter(provider => provider.id() === id).first();
22 | }
23 |
24 | register(provider: FeedProvider): void {
25 | this.providers.push(provider);
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/src/providers/local/LocalFeed.ts:
--------------------------------------------------------------------------------
1 | import {Feed, FeedOrder} from "../Feed";
2 | import {Item} from "../Item";
3 | import {RssFeedContent} from "../../parser/rssParser";
4 | import {LocalFeedItem} from "./LocalFeedItem";
5 |
6 | export class LocalFeed implements Feed {
7 |
8 | private readonly parsed: RssFeedContent;
9 |
10 | constructor(parsed: RssFeedContent) {
11 | this.parsed = parsed;
12 | }
13 |
14 |
15 | favicon(): string {
16 | return this.parsed.image;
17 | }
18 |
19 | folderId(): number {
20 | return this.parsed.folder.length;
21 | }
22 |
23 | folderName(): string {
24 | return this.parsed.folder;
25 | }
26 |
27 | id(): number {
28 | return 0;
29 | }
30 |
31 | items(): Item[] {
32 | const result: Item[] = [];
33 | for (const item of this.parsed.items) {
34 | result.push(new LocalFeedItem(item));
35 | }
36 | return result;
37 | }
38 |
39 | link(): string {
40 | return this.parsed.link;
41 | }
42 |
43 | ordering(): FeedOrder {
44 | return FeedOrder.DEFAULT;
45 | }
46 |
47 | title(): string {
48 | return this.parsed.title;
49 | }
50 |
51 | unreadCount(): number {
52 | return 0;
53 | }
54 |
55 | url(): string {
56 | return this.parsed.link;
57 | }
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/src/providers/local/LocalFeedItem.ts:
--------------------------------------------------------------------------------
1 | import {Item} from "../Item";
2 | import {RssFeedItem} from "../../parser/rssParser";
3 |
4 | export class LocalFeedItem implements Item {
5 |
6 | private readonly item: RssFeedItem;
7 |
8 | constructor(item: RssFeedItem) {
9 | this.item = item;
10 | }
11 |
12 | author(): string {
13 | return this.item.creator;
14 | }
15 |
16 | body(): string {
17 | return this.item.content;
18 | }
19 |
20 | created(): boolean {
21 | return false;
22 | }
23 |
24 | description(): string {
25 | return this.item.description;
26 | }
27 |
28 | enclosureLink(): string {
29 | return "";
30 | }
31 |
32 | enclosureMime(): string {
33 | return "";
34 | }
35 |
36 | feed(): string {
37 | return "";
38 | }
39 |
40 | feedId(): number {
41 | return 0;
42 | }
43 |
44 | folder(): string {
45 | return "";
46 | }
47 |
48 | guid(): string {
49 | return "";
50 | }
51 |
52 | guidHash(): string {
53 | return "";
54 | }
55 |
56 | highlights(): string[] {
57 | return [];
58 | }
59 |
60 | id(): string | number {
61 | return undefined;
62 | }
63 |
64 | language(): string | undefined {
65 | return this.item.language;
66 | }
67 |
68 | markCreated(created: boolean): void {
69 |
70 | }
71 |
72 | markRead(read: boolean): void {
73 | }
74 |
75 | markStarred(starred: boolean): void {
76 | }
77 |
78 | mediaDescription(): string {
79 | return "";
80 | }
81 |
82 | mediaThumbnail(): string {
83 | return "";
84 | }
85 |
86 | pubDate(): string {
87 | return this.item.pubDate;
88 | }
89 |
90 | read(): boolean {
91 | return false;
92 | }
93 |
94 | rtl(): boolean {
95 | return false;
96 | }
97 |
98 | setTags(tags: string[]): void {
99 | }
100 |
101 | starred(): boolean {
102 | return false;
103 | }
104 |
105 | tags(): string[] {
106 | return [];
107 | }
108 |
109 | title(): string {
110 | return this.item.title;
111 | }
112 |
113 | url(): string {
114 | return this.item.link;
115 | }
116 |
117 | }
118 |
--------------------------------------------------------------------------------
/src/providers/local/LocalFeedProvider.ts:
--------------------------------------------------------------------------------
1 | import {FeedProvider} from "../FeedProvider";
2 | import {Feed} from "../Feed";
3 | import {Folder} from "../Folder";
4 | import {Item} from "../Item";
5 | import RssReaderPlugin from "../../main";
6 | import {SettingsSection} from "../../settings/SettingsSection";
7 | import {LocalFeedSettings} from "./LocalFeedSettings";
8 | import {LocalFeed} from "./LocalFeed";
9 | import groupBy from "lodash.groupby";
10 | import {LocalFolder} from "./LocalFolder";
11 | import {getFeedItems} from "../../parser/rssParser";
12 |
13 | export class LocalFeedProvider implements FeedProvider {
14 | private readonly plugin: RssReaderPlugin;
15 |
16 | constructor(plugin: RssReaderPlugin) {
17 | this.plugin = plugin;
18 | }
19 |
20 | async isValid(): Promise {
21 | return true;
22 | }
23 |
24 | id(): string {
25 | return "local";
26 | }
27 |
28 | name(): string {
29 | return "Local";
30 | }
31 |
32 | async feeds(): Promise {
33 | const result: Feed[] = [];
34 | const feeds = this.plugin.settings.feeds;
35 | for (const feed of feeds) {
36 | const content = await getFeedItems(feed);
37 | result.push(new LocalFeed(content));
38 | }
39 |
40 | return result;
41 | }
42 |
43 | async feedFromUrl(url: string): Promise {
44 | const feed = {
45 | name: '',
46 | url,
47 | folder: '',
48 | }
49 | const content = await getFeedItems(feed);
50 | return new LocalFeed(content);
51 | }
52 |
53 | async filteredFolders(): Promise {
54 | return [];
55 | }
56 |
57 |
58 | async folders(): Promise {
59 | const result: Folder[] = [];
60 | const feeds = await this.feeds();
61 | const grouped = groupBy(feeds, item => item.folderName());
62 |
63 | for (const key of Object.keys(grouped)) {
64 | const folderContent = grouped[key];
65 | result.push(new LocalFolder(key, folderContent));
66 | }
67 | return result;
68 | }
69 |
70 | async items(): Promise
- {
71 | return [];
72 | }
73 |
74 | warnings(): string[] {
75 | return [];
76 | }
77 |
78 | settings(containerEl: HTMLDivElement): SettingsSection {
79 | return new LocalFeedSettings(this.plugin, containerEl);
80 | }
81 |
82 | }
83 |
--------------------------------------------------------------------------------
/src/providers/local/LocalFolder.ts:
--------------------------------------------------------------------------------
1 | import {Folder} from "../Folder";
2 | import {Feed} from "../Feed";
3 |
4 | export class LocalFolder implements Folder {
5 |
6 | private readonly _name: string;
7 | private readonly _feeds: Feed[];
8 |
9 | constructor(name: string, feeds: Feed[]) {
10 | this._name = name;
11 | this._feeds = feeds;
12 | }
13 |
14 | feeds(): Feed[] {
15 | return this._feeds;
16 | }
17 |
18 | id(): number {
19 | return 0;
20 | }
21 |
22 | name(): string {
23 | return this._name;
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/src/providers/nextcloud/NextCloudFeed.ts:
--------------------------------------------------------------------------------
1 | import {Feed, FeedOrder} from "../Feed";
2 | import {NextcloudFeedProvider} from "./NextcloudFeedProvider";
3 | import {Item} from "../Item";
4 |
5 | export class NextCloudFeed implements Feed {
6 |
7 | private readonly provider: NextcloudFeedProvider;
8 | private readonly json: any;
9 | private readonly _items: Item[];
10 |
11 | constructor(provider: NextcloudFeedProvider, json: any, items: Item[]) {
12 | this.provider = provider;
13 | this.json = json;
14 | this._items = items;
15 | }
16 |
17 | favicon(): string {
18 | return this.json.faviconLink;
19 | }
20 |
21 | id(): number {
22 | return this.json.id;
23 | }
24 |
25 | link(): string {
26 | return this.json.link;
27 | }
28 |
29 | ordering(): FeedOrder {
30 | return this.json.ordering;
31 | }
32 |
33 | title(): string {
34 | return this.json.title;
35 | }
36 |
37 | unreadCount(): number {
38 | return this.json.unreadCount;
39 | }
40 |
41 | url(): string {
42 | return this.json.url;
43 | }
44 |
45 | folderId(): number {
46 | return this.json.folderId;
47 | }
48 |
49 | folderName(): string {
50 | return this.json.folderName;
51 | }
52 |
53 | items(): Item[] {
54 | return this._items;
55 | }
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/src/providers/nextcloud/NextCloudFeedSettings.ts:
--------------------------------------------------------------------------------
1 | import {SettingsSection} from "../../settings/SettingsSection";
2 | import {Setting} from "obsidian";
3 | import RssReaderPlugin from "../../main";
4 | import {NextcloudFeedProvider} from "./NextcloudFeedProvider";
5 | import {ProviderValidation} from "../../settings/ProviderValidation";
6 |
7 | export class NextCloudFeedSettings extends SettingsSection {
8 | private readonly provider: NextcloudFeedProvider;
9 | private readonly validation: ProviderValidation;
10 |
11 | constructor(plugin: RssReaderPlugin, containerEl: HTMLDivElement, provider: NextcloudFeedProvider) {
12 | super(plugin, containerEl, false);
13 | this.provider = provider;
14 |
15 | this.validation = new ProviderValidation(this.provider, this.contentEl);
16 | }
17 |
18 | getName(): string {
19 | return "";
20 | }
21 |
22 | async display() {
23 | this.contentEl.empty();
24 |
25 | const authData = this.provider.getAuthData();
26 |
27 | new Setting(this.contentEl)
28 | .setName("Server")
29 | .addText(text => {
30 | text
31 | .setPlaceholder("https://your-nextcloud.server.zyx")
32 | .setValue(authData.server)
33 | .onChange(value => {
34 | localStorage.setItem(this.provider.server_key, value);
35 | });
36 | });
37 |
38 | new Setting(this.contentEl)
39 | .setName("Username")
40 | .addText(text => {
41 | text
42 | .setPlaceholder("your-username")
43 | .setValue(authData.username)
44 | .onChange(value => {
45 | localStorage.setItem(this.provider.user_key, value);
46 | });
47 | });
48 |
49 | new Setting(this.contentEl)
50 | .setName("Password")
51 | .addText(text => {
52 | text
53 | .setPlaceholder("your password")
54 | .setValue(authData.password)
55 | .onChange(value => {
56 | localStorage.setItem(this.provider.password_key, value);
57 | });
58 | text.inputEl.type = "password";
59 | });
60 |
61 | await this.validation.display();
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/providers/nextcloud/NextCloudFolder.ts:
--------------------------------------------------------------------------------
1 | import {Folder} from "../Folder";
2 | import {Feed} from "../Feed";
3 |
4 | export class NextCloudFolder implements Folder {
5 | private readonly json: any;
6 | private readonly _feeds: Feed[];
7 |
8 | constructor(json: any, feeds: Feed[]) {
9 | this.json = json;
10 | this._feeds = feeds;
11 | }
12 |
13 | id(): number {
14 | return this.json.id;
15 | }
16 |
17 | name(): string {
18 | return this.json.name;
19 | }
20 |
21 | feeds(): Feed[] {
22 | return this._feeds;
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/src/providers/nextcloud/NextCloudItem.ts:
--------------------------------------------------------------------------------
1 | import {Item} from "../Item";
2 | import {NextcloudFeedProvider} from "./NextcloudFeedProvider";
3 |
4 | export class NextCloudItem implements Item{
5 |
6 | private readonly provider: NextcloudFeedProvider;
7 | private readonly json: any;
8 |
9 | constructor(provider: NextcloudFeedProvider, json: any) {
10 | this.provider = provider;
11 | this.json = json;
12 | }
13 |
14 | public author(): string {
15 | return this.json.author;
16 | }
17 |
18 | public body(): string {
19 | return this.json.body;
20 | }
21 |
22 | public enclosureLink(): string {
23 | return this.json.enclosureLink;
24 | }
25 |
26 | public enclosureMime(): string {
27 | return this.json.enclosureMime;
28 | }
29 |
30 | public feedId(): number {
31 | return this.json.feedId;
32 | }
33 |
34 | public id(): number {
35 | return this.json.id;
36 | }
37 |
38 | public guid(): string {
39 | return this.json.guid;
40 | }
41 |
42 | public guidHash(): string {
43 | return this.json.guidHash;
44 | }
45 |
46 | public mediaDescription(): string {
47 | return this.json.mediaDescription;
48 | }
49 |
50 | public mediaThumbnail(): string {
51 | return this.json.mediaThumbnail;
52 | }
53 |
54 | public pubDate(): string {
55 | return this.json.pubDate;
56 | }
57 |
58 | public read(): boolean {
59 | return !this.json.unread;
60 | }
61 |
62 | public rtl(): boolean {
63 | return this.json.rtl;
64 | }
65 |
66 | public starred(): boolean {
67 | return this.json.starred;
68 | }
69 |
70 | public title(): string {
71 | return this.json.title;
72 | }
73 |
74 | public url(): string {
75 | return this.json.url;
76 | }
77 |
78 | tags(): string[] {
79 | return [];
80 | }
81 |
82 | setTags(tags: string[]) {
83 |
84 | }
85 |
86 | created(): boolean {
87 | return false;
88 | }
89 |
90 | markCreated(created: boolean) {
91 |
92 | }
93 |
94 | language(): string | undefined {
95 | return undefined;
96 | }
97 |
98 | highlights(): string[] {
99 | return [];
100 | }
101 |
102 | description(): string {
103 | return "";
104 | }
105 |
106 | folder(): string {
107 | return "";
108 | }
109 |
110 | feed(): string {
111 | return "";
112 | }
113 |
114 | public async markStarred(starred: boolean) {
115 | this.json.starred = starred;
116 | if(starred) {
117 | await this.provider.putData(`items/${this.feedId()}/${this.guidHash()}/star`);
118 | }else {
119 | await this.provider.putData(`items/${this.feedId()}/${this.guidHash()}/unstar`);
120 | }
121 | }
122 |
123 | public async markRead(read: boolean) {
124 | this.json.unread = !read;
125 | if(read) {
126 | await this.provider.putData(`items/${this.id()}/read`);
127 | }else {
128 | await this.provider.putData(`items/${this.id()}/unread`);
129 | }
130 | }
131 |
132 | }
133 |
--------------------------------------------------------------------------------
/src/providers/nextcloud/NextcloudFeedProvider.ts:
--------------------------------------------------------------------------------
1 | import {FeedProvider} from "../FeedProvider";
2 | import {Folder} from "../Folder";
3 | import {Feed} from "../Feed";
4 | import RssReaderPlugin from "../../main";
5 | import { requestUrl, RequestUrlResponse} from "obsidian";
6 | import {Item} from "../Item";
7 | import {NextCloudItem} from "./NextCloudItem";
8 | import {NextCloudFeed} from "./NextCloudFeed";
9 | import {NextCloudFolder} from "./NextCloudFolder";
10 | import {NextCloudFeedSettings} from "./NextCloudFeedSettings";
11 | import {SettingsSection} from "../../settings/SettingsSection";
12 |
13 | interface NextcloudAuthData {
14 | server: string;
15 | username: string;
16 | password: string;
17 | header: string;
18 | }
19 |
20 | export class NextcloudFeedProvider implements FeedProvider {
21 | private readonly plugin: RssReaderPlugin;
22 |
23 | public readonly server_key = "rss-nc-server";
24 | public readonly user_key = "rss-nc-user";
25 | public readonly password_key = "rss-nc-password";
26 | private readonly path = "/index.php/apps/news/api/v1-2/";
27 |
28 | private _warnings: string[] = [];
29 |
30 | constructor(plugin: RssReaderPlugin) {
31 | this.plugin = plugin;
32 | }
33 |
34 | getAuthData(): NextcloudAuthData {
35 | const username = localStorage.getItem(this.user_key);
36 | const password = localStorage.getItem(this.password_key);
37 |
38 | const header = username + ":" + password;
39 |
40 | return {
41 | server: localStorage.getItem(this.server_key),
42 | username: username,
43 | password: password,
44 | header: btoa(header)
45 | }
46 | }
47 |
48 | getRequestUrl(endpoint: string): string {
49 | const authData = this.getAuthData();
50 | return authData.server + this.path + endpoint;
51 | }
52 |
53 |
54 | async requestData(endpoint: string): Promise {
55 | const authData = this.getAuthData();
56 | return requestUrl({
57 | url: this.getRequestUrl(endpoint),
58 | headers: {
59 | "Content-Type": "application/json",
60 | "Authorization": "Basic " + authData.header,
61 | }
62 | });
63 | }
64 |
65 | async putData(endpoint: string, body?: any) : Promise {
66 | const authData = this.getAuthData();
67 | return requestUrl({
68 | url: this.getRequestUrl(endpoint),
69 | method: "PUT",
70 | headers: {
71 | "Content-Type": "application/json",
72 | "Authorization": "Basic " + authData.header,
73 | },
74 | body
75 | });
76 | }
77 |
78 | id(): string {
79 | return "nextcloud";
80 | }
81 |
82 |
83 | async isValid(): Promise {
84 | this._warnings = [];
85 | try {
86 | const data = await this.requestData("status");
87 | if (data.status !== 200) {
88 | this._warnings.push("Server responded with status code: " + data.status);
89 | return false;
90 | }
91 |
92 | if (data.json.warnings.improperlyConfiguredCron) {
93 | this._warnings.push("The NextCloud News App updater is improperly configured and you will lose updates.\n" +
94 | "See " + this.getAuthData().server + "/index.php/apps/news for instructions on how to fix it.");
95 | return false;
96 | }
97 |
98 | if (data.json.warnings.incorrectDbCharset) {
99 | this._warnings.push("Your NextCloud database is not properly configured, feed updates with unicode characters might fail");
100 | return false;
101 | }
102 | return true;
103 | } catch (e) {
104 | console.log(e);
105 | this._warnings.push("Could not connect to server");
106 | }
107 |
108 | return false;
109 | }
110 |
111 | warnings(): string[] {
112 | return this._warnings;
113 | }
114 |
115 |
116 | name(): string {
117 | return "NextCloud News";
118 | }
119 |
120 | async feeds(): Promise {
121 | const data = await this.requestData("feeds");
122 | const feeds: NextCloudFeed[] = [];
123 | const items = await this.items();
124 | for(const feed of data.json.feeds) {
125 | const feedItems = items.filter(item => item.feedId() === feed.id);
126 | feeds.push(new NextCloudFeed(this, feed, feedItems));
127 | }
128 | return feeds;
129 | }
130 |
131 | async filteredFolders(): Promise {
132 | return [];
133 | }
134 |
135 | async folders(): Promise {
136 | const data = await this.requestData("folders");
137 | const folders: NextCloudFolder[] = [];
138 | const feeds = await this.feeds();
139 | for(const folder of data.json.folders) {
140 | const folderFeeds = feeds.filter(feed => feed.folderId() === folder.id);
141 | folders.push(new NextCloudFolder(this, folder, folderFeeds));
142 | }
143 | return folders;
144 | }
145 |
146 | async items(): Promise
- {
147 | const data = await this.requestData("items");
148 | const items: NextCloudItem[] = [];
149 | for (const item of data.json.items) {
150 | items.push(new NextCloudItem(this, item));
151 | }
152 | return items;
153 | }
154 |
155 | settings(containerEl: HTMLDivElement): SettingsSection {
156 | return new NextCloudFeedSettings(this.plugin, containerEl, this);
157 | }
158 |
159 |
160 | }
161 |
--------------------------------------------------------------------------------
/src/settings/AdvancedSettings.ts:
--------------------------------------------------------------------------------
1 | import {SettingsSection} from "./SettingsSection";
2 | import t from "../l10n/locale";
3 | import {Setting} from "obsidian";
4 |
5 | export class AdvancedSettings extends SettingsSection {
6 |
7 | getName(): string {
8 | return t('advanced');
9 | }
10 |
11 | display() {
12 | this.contentEl.createEl("h4", {text: t("customize_terms")});
13 | this.contentEl.createSpan({text: "Change a few selected terms here. You can help translating the plugin "});
14 | this.contentEl.createEl("a", {text: "here", href: "https://github.com/joethei/obsidian-rss/tree/master/src/l10n"});
15 |
16 | new Setting(this.contentEl)
17 | .setName(t("folders"))
18 | .addText(text => {
19 | text
20 | .setPlaceholder(t("folders"))
21 | .setValue(this.plugin.settings.renamedText.folders)
22 | .onChange(async value => {
23 | await this.plugin.writeSettings(() => ({
24 | renamedText: {
25 | ...this.plugin.settings.renamedText,
26 | folders: value
27 | }
28 | }));
29 | });
30 | });
31 |
32 | new Setting(this.contentEl)
33 | .setName(t("filtered_folders"))
34 | .addText(text => {
35 | text
36 | .setPlaceholder(t("filtered_folders"))
37 | .setValue(this.plugin.settings.renamedText.filtered_folders)
38 | .onChange(async value => {
39 | await this.plugin.writeSettings(() => ({
40 | renamedText: {
41 | ...this.plugin.settings.renamedText,
42 | filtered_folders: value
43 | }
44 | }));
45 | });
46 | });
47 |
48 | new Setting(this.contentEl)
49 | .setName(t("no_folder"))
50 | .addText(text => {
51 | text
52 | .setPlaceholder(t("no_folder"))
53 | .setValue(this.plugin.settings.renamedText.no_folder)
54 | .onChange(async value => {
55 | await this.plugin.writeSettings(() => ({
56 | renamedText: {
57 | ...this.plugin.settings.renamedText,
58 | no_folder: value
59 | }
60 | }));
61 | });
62 | });
63 |
64 | this.contentEl.createEl("hr", {cls: "rss-divider"});
65 |
66 | new Setting(this.contentEl)
67 | .setName(t("display_media"))
68 | .addToggle(toggle => {
69 | toggle
70 | .setValue(this.plugin.settings.displayMedia)
71 | .onChange(async value => {
72 | await this.plugin.writeSettings(() => ({
73 | displayMedia: value
74 | }));
75 | });
76 | });
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/settings/FileCreationSettings.ts:
--------------------------------------------------------------------------------
1 | import {SettingsSection} from "./SettingsSection";
2 | import t from "../l10n/locale";
3 | import {
4 | DropdownComponent,
5 | MomentFormatComponent, Notice,
6 | SearchComponent,
7 | Setting,
8 | TextAreaComponent,
9 | ToggleComponent
10 | } from "obsidian";
11 | import {DEFAULT_SETTINGS} from "./settings";
12 | import {FolderSuggest} from "./FolderSuggestor";
13 |
14 | export class FileCreationSettings extends SettingsSection {
15 |
16 | getName(): string {
17 | return t('file_creation');
18 | }
19 |
20 | display(): void {
21 | this.contentEl.empty();
22 |
23 | const templateDesc = new DocumentFragment();
24 | templateDesc.createDiv().innerHTML = t("template_new_help") + "
" + t("available_variables") + `
` +
25 | `{{title}} → ${t("article_title")}
` +
26 | `{{link}} → ${t("article_link")}
` +
27 | `{{author}} → ${t("article_author")}
` +
28 | `{{published}} → ${t("article_published")}
` +
29 | `{{created}} → ${t("note_created")}
` +
30 | `{{description}} → ${t("article_description")}
` +
31 | `{{content}} → ${t("article_content")}
` +
32 | `{{folder}} → ${t("feed_folder")}
` +
33 | `{{feed}} → ${t("feed_title")}
` +
34 | `{{filename}} → ${t("filename")}
` +
35 | `{{tags}} → ${t("article_tags")}
` +
36 | `{{media}} → ${t("article_media")}
` +
37 | `{{highlights}} → ${t("highlights")}`;
38 |
39 | new Setting(this.contentEl)
40 | .setName(t("template_new"))
41 | .setDesc(templateDesc)
42 | .addTextArea((textArea: TextAreaComponent) => {
43 | textArea
44 | .setValue(this.plugin.settings.template)
45 | .setPlaceholder(DEFAULT_SETTINGS.template)
46 | .onChange(async (value) => {
47 | await this.plugin.writeSettings(() => ({
48 | template: value
49 | }));
50 | });
51 | textArea.inputEl.setAttr("rows", 15);
52 | textArea.inputEl.setAttr("cols", 50);
53 | });
54 |
55 | const pasteTemplateDesc = new DocumentFragment();
56 | pasteTemplateDesc.createDiv().innerHTML = t("template_new_help") + "
" + t("available_variables") + `
` +
57 | `{{title}} → ${t("article_title")}
` +
58 | `{{link}} → ${t("article_link")}
` +
59 | `{{author}} → ${t("article_author")}
` +
60 | `{{published}} → ${t("article_published")}
` +
61 | `{{created}} → ${t("note_created")}
` +
62 | `{{description}} → ${t("article_description")}
` +
63 | `{{content}} → ${t("article_content")}
` +
64 | `{{folder}} → ${t("feed_folder")}
` +
65 | `{{feed}} → ${t("feed_title")}
` +
66 | `{{tags}} → ${t("article_tags")}
` +
67 | `{{media}} → ${t("article_media")}
` +
68 | `{{highlights}} → ${t("highlights")}`;
69 |
70 | new Setting(this.contentEl)
71 | .setName(t("template_paste"))
72 | .setDesc(pasteTemplateDesc)
73 | .addTextArea((textArea: TextAreaComponent) => {
74 | textArea
75 | .setValue(this.plugin.settings.pasteTemplate)
76 | .setPlaceholder(DEFAULT_SETTINGS.pasteTemplate)
77 | .onChange(async (value) => {
78 | await this.plugin.writeSettings(() => ({
79 | pasteTemplate: value
80 | }));
81 | });
82 | textArea.inputEl.setAttr("rows", 15);
83 | textArea.inputEl.setAttr("cols", 50);
84 | });
85 |
86 | new Setting(this.contentEl)
87 | .setName(t("file_location"))
88 | .setDesc(t("file_location_help"))
89 | .addDropdown(async (dropdown: DropdownComponent) => {
90 | dropdown
91 | .addOption("default", t("file_location_default"))
92 | .addOption("custom", t("file_location_custom"))
93 | .setValue(this.plugin.settings.saveLocation)
94 | .onChange(async (value: string) => {
95 | await this.plugin.writeSettings(() => (
96 | {saveLocation: value}
97 | ));
98 | this.display();
99 | });
100 | });
101 |
102 | if (this.plugin.settings.saveLocation == "custom") {
103 | new Setting(this.contentEl)
104 | .setName(t("file_location_folder"))
105 | .setDesc(t("file_location_folder_help"))
106 | .addSearch(async (search: SearchComponent) => {
107 | new FolderSuggest(this.plugin.app, search.inputEl);
108 | search
109 | .setValue(this.plugin.settings.saveLocationFolder)
110 | .setPlaceholder(DEFAULT_SETTINGS.saveLocationFolder)
111 | .onChange(async (value: string) => {
112 | await this.plugin.writeSettings(() => (
113 | {saveLocationFolder: value}
114 | ));
115 | });
116 | });
117 | }
118 |
119 | let dateFormatSampleEl: MomentFormatComponent;
120 | const dateFormat = new Setting(this.contentEl)
121 | .setName(t("date_format"))
122 | .addMomentFormat((format: MomentFormatComponent) => {
123 | dateFormatSampleEl = format
124 | .setDefaultFormat(DEFAULT_SETTINGS.dateFormat)
125 | .setPlaceholder(DEFAULT_SETTINGS.dateFormat)
126 | .setValue(this.plugin.settings.dateFormat)
127 | .onChange(async (value) => {
128 | await this.plugin.writeSettings(() => (
129 | {dateFormat: value}
130 | ));
131 | });
132 | });
133 | const referenceLink = dateFormat.descEl.createEl("a");
134 | referenceLink.setAttr("href", "https://momentjs.com/docs/#/displaying/format/");
135 | referenceLink.setText(t("syntax_reference"));
136 | const text = dateFormat.descEl.createDiv("text");
137 | text.setText(t("syntax_looks"));
138 | const sampleEl = text.createSpan("sample");
139 | dateFormatSampleEl.setSampleEl(sampleEl);
140 | dateFormat.addExtraButton((button) => {
141 | button
142 | .setIcon('reset')
143 | .setTooltip(t("reset"))
144 | .onClick(async () => {
145 | await this.plugin.writeSettings(() => ({
146 | dateFormat: DEFAULT_SETTINGS.dateFormat
147 | }));
148 | this.display();
149 | });
150 | });
151 |
152 | new Setting(this.contentEl)
153 | .setName(t("ask_filename"))
154 | .setDesc(t("ask_filename_help"))
155 | .addToggle((toggle: ToggleComponent) => {
156 | toggle
157 | .setValue(this.plugin.settings.askForFilename)
158 | .onChange(async (value) => {
159 | await this.plugin.writeSettings(() => ({
160 | askForFilename: value
161 | }));
162 | });
163 | });
164 |
165 | new Setting(this.contentEl)
166 | .setName(t("default_filename"))
167 | .setDesc(t("default_filename_help"))
168 | .addText((text) => {
169 | text
170 | .setPlaceholder(DEFAULT_SETTINGS.defaultFilename)
171 | .setValue(this.plugin.settings.defaultFilename)
172 | .onChange(async (value) => {
173 | if (value.length > 0) {
174 | await this.plugin.writeSettings(() => ({
175 | defaultFilename: value
176 | }));
177 | } else {
178 | new Notice(t("fix_errors"));
179 | }
180 | });
181 | });
182 | }
183 |
184 | }
185 |
--------------------------------------------------------------------------------
/src/settings/FilterSettings.ts:
--------------------------------------------------------------------------------
1 | import t from "../l10n/locale";
2 | import {ButtonComponent, Notice, Setting} from "obsidian";
3 | import {FilteredFolderModal} from "../modals/FilteredFolderModal";
4 | import {SettingsSection} from "./SettingsSection";
5 |
6 | export class FilterSettings extends SettingsSection {
7 | getName(): string {
8 | return t('filtered_folders');
9 | }
10 |
11 | display() {
12 |
13 | this.contentEl.empty();
14 |
15 | new Setting(this.contentEl)
16 | .setName(t("add_new"))
17 | .setDesc(t("add_new_filter"))
18 | .addButton((button: ButtonComponent): ButtonComponent => {
19 | return button
20 | .setTooltip(t("add_new_filter"))
21 | .setIcon("plus")
22 | .onClick(async () => {
23 | const modal = new FilteredFolderModal(this.plugin);
24 |
25 | modal.onClose = async () => {
26 | if (modal.saved) {
27 | if (this.plugin.settings.filtered.some(folder => folder.name === modal.name)) {
28 | new Notice(t("filter_exists"));
29 | return;
30 | }
31 | await this.plugin.writeFiltered(() => (
32 | this.plugin.settings.filtered.concat({
33 | name: modal.name,
34 | sortOrder: modal.sortOrder,
35 | filterFeeds: modal.filterFeeds,
36 | filterFolders: modal.filterFolders,
37 | filterTags: modal.filterTags,
38 | favorites: modal.favorites,
39 | ignoreFolders: modal.ignoreFolders,
40 | ignoreFeeds: modal.ignoreFeeds,
41 | ignoreTags: modal.ignoreTags,
42 | read: modal.read,
43 | unread: modal.unread,
44 | }
45 | )));
46 | this.display();
47 | }
48 | };
49 |
50 | modal.open();
51 | });
52 | });
53 |
54 | const filterContainer = this.contentEl.createDiv(
55 | "filter-container"
56 | );
57 | const filtersDiv = filterContainer.createDiv("filters");
58 | for (const id in this.plugin.settings.filtered.sort((a, b) => a.name.localeCompare(b.name))) {
59 | const filter = this.plugin.settings.filtered[id];
60 | if (filter === undefined) {
61 | continue;
62 | }
63 | const setting = new Setting(filtersDiv);
64 |
65 | setting.setName(filter.name);
66 |
67 | const description: string[] = [];
68 | if (filter.read)
69 | description.push(t("read"));
70 | if (filter.unread)
71 | description.push(t("unread"));
72 | if (filter.favorites)
73 | description.push(t("favorites"));
74 |
75 | let message = "";
76 | if (filter.filterFolders !== undefined && filter.filterFolders.length > 0) {
77 | const folders = filter.filterFolders.join(",");
78 | message += "; " + t("from_folders") + folders;
79 | }
80 | if (filter.filterFeeds !== undefined && filter.filterFeeds.length > 0) {
81 | const feeds = filter.filterFeeds.join(",");
82 | message += "; " + t("from_feeds") + feeds;
83 | }
84 | if (filter.filterTags !== undefined && filter.filterTags.length > 0) {
85 | const tags = filter.filterTags.join(",");
86 | message += "; " + t("with_tags") + tags;
87 | }
88 |
89 | setting.setDesc(description.join(",") + message);
90 |
91 |
92 | setting
93 | .addExtraButton((b) => {
94 | b.setIcon("edit")
95 | .setTooltip(t("edit"))
96 | .onClick(() => {
97 | const modal = new FilteredFolderModal(this.plugin, filter);
98 | const oldFilter = filter;
99 |
100 | modal.onClose = async () => {
101 | if (modal.saved) {
102 | const filters = this.plugin.settings.filtered;
103 | filters.remove(oldFilter);
104 | filters.push({
105 | name: modal.name,
106 | sortOrder: modal.sortOrder,
107 | filterFeeds: modal.filterFeeds,
108 | filterFolders: modal.filterFolders,
109 | filterTags: modal.filterTags,
110 | ignoreFolders: modal.ignoreFolders,
111 | ignoreFeeds: modal.ignoreFeeds,
112 | ignoreTags: modal.ignoreTags,
113 | favorites: modal.favorites,
114 | read: modal.read,
115 | unread: modal.unread,
116 | });
117 | await this.plugin.writeFiltered(() => (filters));
118 | this.display();
119 | }
120 | };
121 |
122 | modal.open();
123 | });
124 | })
125 | .addExtraButton((b) => {
126 | b.setIcon("lucide-trash")
127 | .setTooltip(t("delete"))
128 | .onClick(async () => {
129 | const filters = this.plugin.settings.filtered;
130 | filters.remove(filter);
131 | await this.plugin.writeFiltered(() => (filters));
132 | this.display();
133 | });
134 | });
135 | }
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/src/settings/FolderSuggestor.ts:
--------------------------------------------------------------------------------
1 | // Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes
2 |
3 | import { TAbstractFile, TFolder } from "obsidian";
4 | import { TextInputSuggest } from "./suggest";
5 |
6 | export class FolderSuggest extends TextInputSuggest {
7 | getSuggestions(inputStr: string): TFolder[] {
8 | const abstractFiles = this.app.vault.getAllLoadedFiles();
9 | const folders: TFolder[] = [];
10 | const lowerCaseInputStr = inputStr.toLowerCase();
11 |
12 | abstractFiles.forEach((folder: TAbstractFile) => {
13 | if (
14 | folder instanceof TFolder &&
15 | folder.path.toLowerCase().contains(lowerCaseInputStr)
16 | ) {
17 | folders.push(folder);
18 | }
19 | });
20 |
21 | return folders;
22 | }
23 |
24 | renderSuggestion(file: TFolder, el: HTMLElement): void {
25 | el.setText(file.path);
26 | }
27 |
28 | selectSuggestion(file: TFolder): void {
29 | this.inputEl.value = file.path;
30 | this.inputEl.trigger("input");
31 | this.close();
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/settings/MiscSettings.ts:
--------------------------------------------------------------------------------
1 | import {SettingsSection} from "./SettingsSection";
2 | import t from "../l10n/locale";
3 | import {Notice, Setting, TextComponent, ToggleComponent} from "obsidian";
4 | import {DEFAULT_SETTINGS} from "./settings";
5 |
6 | export class MiscSettings extends SettingsSection {
7 |
8 | getName(): string {
9 | return t('misc');
10 | }
11 |
12 | display() {
13 | this.contentEl.empty();
14 |
15 | const refresh = new Setting(this.contentEl)
16 | .setName(t("refresh_time"))
17 | .setDesc(t("refresh_time_help"))
18 | .addText((text: TextComponent) => {
19 | text
20 | .setPlaceholder(String(DEFAULT_SETTINGS.updateTime))
21 | .setValue(String(this.plugin.settings.updateTime))
22 | .onChange(async (value) => {
23 | if (value.length === 0) {
24 | new Notice(t("specify_positive_number"));
25 | return;
26 | }
27 | if (Number(value) < 0) {
28 | new Notice(t("specify_positive_number"));
29 | return;
30 | }
31 |
32 | await this.plugin.writeSettings(() => (
33 | {updateTime: Number(value)}
34 | ));
35 | });
36 | text.inputEl.setAttr("type", "number");
37 | text.inputEl.setAttr("min", "1");
38 | //we don't want decimal numbers.
39 | text.inputEl.setAttr("onkeypress", "return event.charCode >= 48 && event.charCode <= 57");
40 | });
41 | refresh.addExtraButton((button) => {
42 | button
43 | .setIcon('reset')
44 | .setTooltip('restore default')
45 | .onClick(async () => {
46 | await this.plugin.writeSettings(() => ({
47 | updateTime: DEFAULT_SETTINGS.updateTime
48 | }));
49 | this.display();
50 | });
51 | });
52 |
53 | new Setting(this.contentEl)
54 | .setName(t("multi_device_usage"))
55 | .setDesc(t("multi_device_usage_help"))
56 | .addToggle((toggle: ToggleComponent) => {
57 | return toggle
58 | .setValue(this.plugin.settings.autoSync)
59 | .onChange(async (value) => {
60 | await this.plugin.writeSettings(() => ({
61 | autoSync: value
62 | }));
63 | });
64 | });
65 |
66 | new Setting(this.contentEl)
67 | .setName(t("display_style"))
68 | .addDropdown(dropdown => {
69 | return dropdown
70 | .addOption("list", t("list"))
71 | .addOption("cards", t("cards"))
72 | .setValue(this.plugin.settings.displayStyle)
73 | .onChange(async (value) => {
74 | await this.plugin.writeSettings(() => ({
75 | displayStyle: value
76 | }));
77 | });
78 | });
79 | }
80 |
81 | }
82 |
--------------------------------------------------------------------------------
/src/settings/ProviderSettings.ts:
--------------------------------------------------------------------------------
1 | import {SettingsSection} from "./SettingsSection";
2 | import t from "../l10n/locale";
3 |
4 | export class ProviderSettings extends SettingsSection {
5 |
6 | getName(): string {
7 | return t('content');
8 | }
9 |
10 | display(): void {
11 | this.contentEl.empty();
12 |
13 | /* new Setting(this.contentEl)
14 | .setName(t("provider"))
15 | .addDropdown(dropdown => {
16 | for (const feedProvider of this.plugin.providers.getAll()) {
17 | dropdown.addOption(feedProvider.id(), feedProvider.name());
18 | }
19 | dropdown
20 | .setValue(this.plugin.settings.provider)
21 | .onChange(async (value) => {
22 | this.plugin.settings.provider = value;
23 | await this.plugin.saveSettings();
24 | this.display();
25 | })
26 | });*/
27 |
28 | const providerEl = this.contentEl.createDiv();
29 |
30 | const provider = this.plugin.providers.getCurrent();
31 | provider.settings(providerEl).display();
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/src/settings/ProviderValidation.ts:
--------------------------------------------------------------------------------
1 | import {Setting} from "obsidian";
2 | import {FeedProvider} from "../providers/FeedProvider";
3 |
4 | export class ProviderValidation {
5 | private readonly provider: FeedProvider;
6 | private readonly containerEl: HTMLDivElement;
7 |
8 | constructor(provider: FeedProvider, containerEl: HTMLDivElement) {
9 | this.provider = provider;
10 | this.containerEl = containerEl;
11 | }
12 |
13 | public async display() {
14 | const isValid = await this.provider.isValid();
15 |
16 | new Setting(this.containerEl)
17 | .setName("Validate")
18 | .setDesc("Ensure that the service is configured properly")
19 | .addButton(button => {
20 | button
21 | .setButtonText("Test")
22 | .setIcon(isValid ? "check" : "x")
23 | .setClass(isValid ? "rss-test-valid" : "rss-test-invalid")
24 | .onClick(async () => {
25 | await this.display();
26 | })
27 | }
28 | )
29 |
30 |
31 | if (!isValid) {
32 | this.containerEl.createEl("h4", {text: "Errors"});
33 | const list = this.containerEl.createEl("ul");
34 | for (const warning of this.provider.warnings()) {
35 | list.createEl("li", {text: warning});
36 | }
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/settings/SettingsSection.ts:
--------------------------------------------------------------------------------
1 | import RssReaderPlugin from "../main";
2 |
3 | export abstract class SettingsSection {
4 | protected readonly plugin: RssReaderPlugin;
5 | protected readonly containerEl: HTMLDivElement;
6 | protected readonly contentEl: HTMLDivElement;
7 |
8 | constructor(plugin: RssReaderPlugin, containerEl: HTMLDivElement, divider = true) {
9 | this.plugin = plugin;
10 | this.containerEl = containerEl;
11 |
12 | this.containerEl.createEl('h3', {text: this.getName()});
13 | this.contentEl = this.containerEl.createDiv('settings-section');
14 | if(divider)
15 | this.containerEl.createEl("hr", {cls: "rss-divider"});
16 | }
17 |
18 | public abstract getName() : string;
19 |
20 | public abstract display(): void;
21 | }
22 |
--------------------------------------------------------------------------------
/src/settings/SettingsTab.ts:
--------------------------------------------------------------------------------
1 | import {
2 | App,
3 | PluginSettingTab,
4 | } from "obsidian";
5 | import RssReaderPlugin from "../main";
6 | import {HotkeySettings} from "./HotkeySettings";
7 | import {ProviderSettings} from "./ProviderSettings";
8 | import {FileCreationSettings} from "./FileCreationSettings";
9 | import {AdvancedSettings} from "./AdvancedSettings";
10 | import {MiscSettings} from "./MiscSettings";
11 |
12 | export class RSSReaderSettingsTab extends PluginSettingTab {
13 | plugin: RssReaderPlugin;
14 |
15 | constructor(app: App, plugin: RssReaderPlugin) {
16 | super(app, plugin);
17 | this.plugin = plugin;
18 | }
19 |
20 | display(): void {
21 | const {containerEl} = this;
22 |
23 | containerEl.empty();
24 |
25 | new ProviderSettings(this.plugin, containerEl.createDiv('content')).display();
26 | new FileCreationSettings(this.plugin, containerEl.createDiv('file-creation')).display();
27 | new MiscSettings(this.plugin, containerEl.createDiv('misc')).display();
28 | new HotkeySettings(this.plugin, containerEl.createDiv('hotkeys')).display();
29 | new AdvancedSettings(this.plugin, this.containerEl.createDiv('advanced'), false).display();
30 |
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/settings/settings.ts:
--------------------------------------------------------------------------------
1 | import {FilteredFolder} from "../modals/FilteredFolderModal";
2 | import {RssFeedContent} from "../parser/rssParser";
3 |
4 | export interface RssFeed {
5 | name: string,
6 | url: string,
7 | folder: string
8 | }
9 |
10 | export interface RssReaderSettings {
11 | feeds: RssFeed[],
12 | template: string,
13 | pasteTemplate: string,
14 | updateTime: number,
15 | saveLocation: string,
16 | saveLocationFolder: string,
17 | filtered: FilteredFolder[],
18 | items: RssFeedContent[],
19 | dateFormat: string,
20 | askForFilename: boolean,
21 | defaultFilename: string,
22 | autoSync: boolean,
23 | displayStyle: string,
24 | hotkeys: {
25 | create: string,
26 | paste: string,
27 | copy: string,
28 | favorite: string,
29 | read: string,
30 | tags: string,
31 | open: string,
32 | tts: string,
33 | next: string,
34 | previous: string,
35 | },
36 | folded: string[],
37 | renamedText: {
38 | filtered_folders: string,
39 | folders: string,
40 | no_folder: string,
41 | },
42 | displayMedia: boolean,
43 | provider: string,
44 | }
45 |
46 | export const DEFAULT_SETTINGS: RssReaderSettings = Object.freeze({
47 | feeds: [],
48 | updateTime: 60,
49 | filtered: [{
50 | name: "Favorites",
51 | read: true,
52 | unread: true,
53 | filterTags: [],
54 | filterFolders: [],
55 | filterFeeds: [],
56 | ignoreTags: [],
57 | ignoreFeeds: [],
58 | ignoreFolders: [],
59 | favorites: true,
60 | sortOrder: "ALPHABET_NORMAL"
61 | }],
62 | saveLocation: 'default',
63 | displayStyle: 'cards',
64 | saveLocationFolder: '',
65 | items: [],
66 | dateFormat: "YYYY-MM-DDTHH:mm:SS",
67 | template: "---\n" +
68 | "link: {{link}}\n" +
69 | "author: {{author}}\n" +
70 | "published: {{published}}\n" +
71 | "tags: [{{tags:,}}]\n" +
72 | "---\n" +
73 | "# Highlights\n" +
74 | "{{highlights}}\n\n" +
75 | "---\n" +
76 | "# {{title}}\n" +
77 | "{{content}}",
78 | pasteTemplate: "## {{title}}\n" +
79 | "{{content}}",
80 | askForFilename: false,
81 | defaultFilename: "{{title}}",
82 | autoSync: false,
83 | hotkeys: {
84 | create: "n",
85 | paste: "v",
86 | copy: "c",
87 | favorite: "f",
88 | read: "r",
89 | tags: "t",
90 | open: "o",
91 | tts: "s",
92 | previous: "ArrowLeft",
93 | next: "ArrowRight"
94 | },
95 | folded: [],
96 | renamedText: {
97 | filtered_folders: "",
98 | folders: "",
99 | no_folder: ""
100 | },
101 | displayMedia: true,
102 | provider: "local"
103 | });
104 |
105 |
--------------------------------------------------------------------------------
/src/settings/suggest.ts:
--------------------------------------------------------------------------------
1 | // Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes
2 |
3 | import { App, ISuggestOwner, Scope } from "obsidian";
4 | import { createPopper, Instance as PopperInstance } from "@popperjs/core";
5 |
6 | const wrapAround = (value: number, size: number): number => {
7 | return ((value % size) + size) % size;
8 | };
9 |
10 | class Suggest {
11 | private owner: ISuggestOwner;
12 | private values: T[];
13 | private suggestions: HTMLDivElement[];
14 | private selectedItem: number;
15 | private containerEl: HTMLElement;
16 |
17 | constructor(
18 | owner: ISuggestOwner,
19 | containerEl: HTMLElement,
20 | scope: Scope
21 | ) {
22 | this.owner = owner;
23 | this.containerEl = containerEl;
24 |
25 | containerEl.on(
26 | "click",
27 | ".suggestion-item",
28 | this.onSuggestionClick.bind(this)
29 | );
30 | containerEl.on(
31 | "mousemove",
32 | ".suggestion-item",
33 | this.onSuggestionMouseover.bind(this)
34 | );
35 |
36 | scope.register([], "ArrowUp", (event) => {
37 | if (!event.isComposing) {
38 | this.setSelectedItem(this.selectedItem - 1, true);
39 | return false;
40 | }
41 | });
42 |
43 | scope.register([], "ArrowDown", (event) => {
44 | if (!event.isComposing) {
45 | this.setSelectedItem(this.selectedItem + 1, true);
46 | return false;
47 | }
48 | });
49 |
50 | scope.register([], "Enter", (event) => {
51 | if (!event.isComposing) {
52 | this.useSelectedItem(event);
53 | return false;
54 | }
55 | });
56 | }
57 |
58 | onSuggestionClick(event: MouseEvent, el: HTMLDivElement): void {
59 | event.preventDefault();
60 |
61 | const item = this.suggestions.indexOf(el);
62 | this.setSelectedItem(item, false);
63 | this.useSelectedItem(event);
64 | }
65 |
66 | onSuggestionMouseover(_event: MouseEvent, el: HTMLDivElement): void {
67 | const item = this.suggestions.indexOf(el);
68 | this.setSelectedItem(item, false);
69 | }
70 |
71 | setSuggestions(values: T[]) {
72 | this.containerEl.empty();
73 | const suggestionEls: HTMLDivElement[] = [];
74 |
75 | values.forEach((value) => {
76 | const suggestionEl = this.containerEl.createDiv("suggestion-item");
77 | this.owner.renderSuggestion(value, suggestionEl);
78 | suggestionEls.push(suggestionEl);
79 | });
80 |
81 | this.values = values;
82 | this.suggestions = suggestionEls;
83 | this.setSelectedItem(0, false);
84 | }
85 |
86 | useSelectedItem(event: MouseEvent | KeyboardEvent) {
87 | const currentValue = this.values[this.selectedItem];
88 | if (currentValue) {
89 | this.owner.selectSuggestion(currentValue, event);
90 | }
91 | }
92 |
93 | setSelectedItem(selectedIndex: number, scrollIntoView: boolean) {
94 | const normalizedIndex = wrapAround(
95 | selectedIndex,
96 | this.suggestions.length
97 | );
98 | const prevSelectedSuggestion = this.suggestions[this.selectedItem];
99 | const selectedSuggestion = this.suggestions[normalizedIndex];
100 |
101 | prevSelectedSuggestion?.removeClass("is-selected");
102 | selectedSuggestion?.addClass("is-selected");
103 |
104 | this.selectedItem = normalizedIndex;
105 |
106 | if (scrollIntoView) {
107 | selectedSuggestion.scrollIntoView(false);
108 | }
109 | }
110 | }
111 |
112 | export abstract class TextInputSuggest implements ISuggestOwner {
113 | protected app: App;
114 | protected inputEl: HTMLInputElement | HTMLTextAreaElement;
115 |
116 | private popper: PopperInstance;
117 | private readonly scope: Scope;
118 | private readonly suggestEl: HTMLElement;
119 | private suggest: Suggest;
120 |
121 | constructor(app: App, inputEl: HTMLInputElement | HTMLTextAreaElement) {
122 | this.app = app;
123 | this.inputEl = inputEl;
124 | this.scope = new Scope();
125 |
126 | this.suggestEl = createDiv("suggestion-container");
127 | const suggestion = this.suggestEl.createDiv("suggestion");
128 | this.suggest = new Suggest(this, suggestion, this.scope);
129 |
130 | this.scope.register([], "Escape", this.close.bind(this));
131 |
132 | this.inputEl.addEventListener("input", this.onInputChanged.bind(this));
133 | this.inputEl.addEventListener("focus", this.onInputChanged.bind(this));
134 | this.inputEl.addEventListener("blur", this.close.bind(this));
135 | this.suggestEl.on(
136 | "mousedown",
137 | ".suggestion-container",
138 | (event: MouseEvent) => {
139 | event.preventDefault();
140 | }
141 | );
142 | }
143 |
144 | onInputChanged(): void {
145 | const inputStr = this.inputEl.value;
146 | const suggestions = this.getSuggestions(inputStr);
147 |
148 | if (!suggestions) {
149 | this.close();
150 | return;
151 | }
152 |
153 | if (suggestions.length > 0) {
154 | this.suggest.setSuggestions(suggestions);
155 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
156 | this.open((this.app).dom.appContainerEl, this.inputEl);
157 | } else {
158 | this.close();
159 | }
160 | }
161 |
162 | open(container: HTMLElement, inputEl: HTMLElement): void {
163 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
164 | (this.app).keymap.pushScope(this.scope);
165 |
166 | container.appendChild(this.suggestEl);
167 | this.popper = createPopper(inputEl, this.suggestEl, {
168 | placement: "bottom-start",
169 | modifiers: [
170 | {
171 | name: "sameWidth",
172 | enabled: true,
173 | fn: ({ state, instance }) => {
174 | // Note: positioning needs to be calculated twice -
175 | // first pass - positioning it according to the width of the popper
176 | // second pass - position it with the width bound to the reference element
177 | // we need to early exit to avoid an infinite loop
178 | const targetWidth = `${state.rects.reference.width}px`;
179 | if (state.styles.popper.width === targetWidth) {
180 | return;
181 | }
182 | state.styles.popper.width = targetWidth;
183 | instance.update();
184 | },
185 | phase: "beforeWrite",
186 | requires: ["computeStyles"],
187 | },
188 | ],
189 | });
190 | }
191 |
192 | close(): void {
193 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
194 | (this.app).keymap.popScope(this.scope);
195 |
196 | this.suggest.setSuggestions([]);
197 | if (this.popper) this.popper.destroy();
198 | this.suggestEl.detach();
199 | }
200 |
201 | abstract getSuggestions(inputStr: string): T[];
202 | abstract renderSuggestion(item: T, el: HTMLElement): void;
203 | abstract selectSuggestion(item: T): void;
204 | }
205 |
--------------------------------------------------------------------------------
/src/stores.ts:
--------------------------------------------------------------------------------
1 | import {writable} from "svelte/store";
2 | import {DEFAULT_SETTINGS, RssFeed, RssReaderSettings} from "./settings/settings";
3 | import {RssFeedContent, RssFeedItem} from "./parser/rssParser";
4 | import Array from "obsidian";
5 | import {FilteredFolder} from "./modals/FilteredFolderModal";
6 |
7 | export interface FeedItems {
8 | items: RssFeedItem[];
9 | }
10 | export interface FilteredFolderContent {
11 | filter: FilteredFolder;
12 | items: FeedItems;
13 | }
14 |
15 | export const configuredFeedsStore = writable>([]);
16 | export const filteredStore = writable>([]);
17 | export const settingsStore = writable(DEFAULT_SETTINGS);
18 |
19 | export const feedsStore = writable([]);
20 | export const sortedFeedsStore = writable<_.Dictionary>();
21 | export const filteredItemsStore = writable>();
22 |
23 | export const foldedState = writable>();
24 | export const tagsStore = writable>();
25 | export const folderStore = writable>();
26 |
--------------------------------------------------------------------------------
/src/style/main.scss:
--------------------------------------------------------------------------------
1 | .rss-read a {
2 | color: darkslategrey;
3 | }
4 |
5 | .has-invalid-message {
6 | display: grid;
7 | grid-template-columns: 1fr 1fr;
8 | grid-template-rows: 1fr 1fr;
9 | grid-template-areas:
10 | "text image"
11 | "inv inv";
12 | }
13 |
14 | .rss-setting-input input {
15 | grid-column: span 2;
16 | }
17 |
18 | input.is-invalid {
19 | border-color: #dc3545 !important;
20 | background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");
21 | background-repeat: no-repeat;
22 | background-position: right calc(0.375em + 0.1875rem) center;
23 | background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
24 | }
25 |
26 | .unset-align-items {
27 | align-items: unset;
28 | }
29 |
30 | .invalid-feedback {
31 | display: block;
32 | grid-area: inv;
33 | width: 100%;
34 | margin-top: 0.25rem;
35 | font-size: 0.875em;
36 | color: #dc3545;
37 | }
38 |
39 | .rss-scrollable-content {
40 | overflow: auto;
41 | }
42 |
43 | .rss-content {
44 | height: 60vh;
45 | }
46 |
47 |
48 | .rss-tooltip {
49 | position: relative;
50 | }
51 |
52 | .rss-tooltip .tooltiptext {
53 | visibility: hidden;
54 | background-color: var(--interactive-hover);
55 | color: var(--text-normal);
56 | text-align: center;
57 | padding: 5px 0;
58 | border-radius: 6px;
59 |
60 | position: absolute;
61 | z-index: var(--layer-tooltip);
62 | }
63 |
64 | .rss-tooltip:hover .tooltiptext {
65 | visibility: visible;
66 | }
67 |
68 | .rss-content img {
69 | max-width: 100%;
70 | }
71 |
72 | .rss-subtitle {
73 | display: inline;
74 | }
75 |
76 | /*action buttons on mobile should not take one line each*/
77 | .is-mobile button.rss-button {
78 | width: auto;
79 | }
80 |
81 | .rss-selectable {
82 | user-select: text;
83 | }
84 |
85 | .rss-card {
86 | padding-top: 10px;
87 | width: 100%;
88 | height: 100%;
89 |
90 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
91 | transition: 0.3s;
92 | border-radius: 5px;
93 | background: var(--background-primary);
94 | }
95 |
96 | .rss-card-items {
97 | display: flex;
98 | padding-top: 10px;
99 | }
100 |
101 | .rss-card-items .rss-item-text {
102 | padding-left: 5px;
103 | text-overflow: ellipsis;
104 | overflow: hidden;
105 | max-height: 10em;
106 | }
107 |
108 | .rss-item-title {
109 | padding-left: 5px;
110 | }
111 |
112 | .rss-feed-item {
113 | width: 100%;
114 | }
115 |
116 | .rss-view {
117 | background: var(--background-secondary);
118 | }
119 |
120 | .rss-modal {
121 | width: 1000px;
122 | }
123 |
124 | img.feed-favicon {
125 | height: 1em;
126 | }
127 |
128 | /*making sure both highlight styles look consistent*/
129 | .rss-modal li mark {
130 | background-color: var(--text-highlight-bg);
131 | }
132 |
133 |
134 | .rss-modal mark li {
135 | background-color: var(--text-highlight-bg);
136 | }
137 |
138 | .rss-modal mark {
139 | background-color: var(--text-highlight-bg);
140 | }
141 |
142 | .rss-content .frontmatter {
143 | display: none;
144 | }
145 |
146 | .rss-content .frontmatter-container {
147 | display: none;
148 | }
149 |
150 | .rss-item-count {
151 | margin-left: auto;
152 | margin-right: 0;
153 | }
154 |
155 | .rss-divider {
156 | border-top: 5px solid var(--background-modifier-border);
157 | }
158 |
159 | .modal.mod-settings button:not(.mod-cta):not(.mod-warning).rss-test-valid {
160 | background-color: var(--background-modifier-success);
161 | }
162 |
163 | .modal.mod-settings button:not(.mod-cta):not(.mod-warning).rss-test-invalid {
164 | background-color: var(--background-modifier-error);
165 | }
166 |
167 | .feed ul{
168 | margin-block-start: 0;
169 | }
170 |
--------------------------------------------------------------------------------
/src/view/ArraySuggest.ts:
--------------------------------------------------------------------------------
1 | import {TextInputSuggest} from "../settings/suggest";
2 | import {App} from "obsidian";
3 |
4 | export class ArraySuggest extends TextInputSuggest {
5 |
6 | content: Set;
7 |
8 | constructor(app : App, input: HTMLInputElement, content: Set) {
9 | super(app, input);
10 | this.content = content;
11 | }
12 |
13 | getSuggestions(inputStr: string): string[] {
14 | const lowerCaseInputStr = inputStr.toLowerCase();
15 | return [...this.content].filter((content) => content.contains(lowerCaseInputStr));
16 | }
17 |
18 | renderSuggestion(content: string, el: HTMLElement): void {
19 | el.setText(content);
20 | }
21 |
22 | selectSuggestion(content: string): void {
23 | this.inputEl.value = content;
24 | this.inputEl.trigger("input");
25 | this.close();
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/view/FeedFolderSuggest.ts:
--------------------------------------------------------------------------------
1 | import {TextInputSuggest} from "../settings/suggest";
2 | import {get} from "svelte/store";
3 | import {folderStore} from "../stores";
4 |
5 | export class FeedFolderSuggest extends TextInputSuggest {
6 |
7 | getSuggestions(inputStr: string): string[] {
8 | const folders = get(folderStore);
9 | const lowerCaseInputStr = inputStr.toLowerCase();
10 | return [...folders].filter(folder => folder.contains(lowerCaseInputStr));
11 | }
12 |
13 | renderSuggestion(folder: string, el: HTMLElement): void {
14 | el.setText(folder);
15 | }
16 |
17 | selectSuggestion(folder: string): void {
18 | this.inputEl.value = folder;
19 | this.inputEl.trigger("input");
20 | this.close();
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/view/FeedView.svelte:
--------------------------------------------------------------------------------
1 |
79 |
80 | {#if !feed}
81 | ...loading
82 | {:else}
83 |
84 |
119 |
120 | {/if}
121 |
--------------------------------------------------------------------------------
/src/view/FolderView.svelte:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joethei/obsidian-rss/4cbdc7155c6a6ffddab814cde9d72f317da6f8db/src/view/FolderView.svelte
--------------------------------------------------------------------------------
/src/view/HtmlTooltip.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 | {#if content.length > 0}
15 |
16 | {/if}
17 |
--------------------------------------------------------------------------------
/src/view/IconComponent.svelte:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 | {#if iconName.length > 0}
15 |
16 | {/if}
17 |
--------------------------------------------------------------------------------
/src/view/ItemTitle.svelte:
--------------------------------------------------------------------------------
1 |
47 |
48 | {#if (item.starred())}
49 |
50 | {/if}
51 | {#if (item.created())}
52 |
53 | {/if}
54 | {
55 | new ItemModal(plugin, item, items).open();
56 | }}
57 | on:contextmenu={openMenu}
58 | >
59 | {item.title()}
60 |
61 |
--------------------------------------------------------------------------------
/src/view/ItemView.svelte:
--------------------------------------------------------------------------------
1 |
20 |
21 | {#if item}
22 |
73 | {/if}
74 |
--------------------------------------------------------------------------------
/src/view/MainView.svelte:
--------------------------------------------------------------------------------
1 |
21 |
22 | {#each folders as folder}
23 | {folder.name()}
24 | {#each feeds.filter(feed => feed.folderId() === folder.id()) as feed}
25 |
26 | {/each}
27 |
28 | {/each}
29 |
--------------------------------------------------------------------------------
/src/view/MarkdownContent.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 | {#if content.length > 0}
17 |
18 | {/if}
19 |
--------------------------------------------------------------------------------
/src/view/TopRowButtons.svelte:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/view/ViewLoader.ts:
--------------------------------------------------------------------------------
1 | import {setIcon, View, WorkspaceLeaf} from "obsidian";
2 | import RssReaderPlugin from "../main";
3 | import {VIEW_ID} from "../consts";
4 | import t from "../l10n/locale";
5 | import {ItemModal} from "../modals/ItemModal";
6 |
7 | export default class ViewLoader extends View {
8 | private readonly plugin: RssReaderPlugin;
9 |
10 | private navigationEl: HTMLElement;
11 | private navigationButtonsEl: HTMLElement;
12 | private contentContainer: HTMLDivElement;
13 |
14 | constructor(leaf: WorkspaceLeaf, plugin: RssReaderPlugin) {
15 | super(leaf);
16 | this.plugin = plugin;
17 |
18 | this.navigationEl = this.containerEl.createDiv('nav-header');
19 | this.navigationButtonsEl = this.navigationEl.createDiv('nav-buttons-container');
20 |
21 | this.contentContainer = this.containerEl.createDiv({cls: 'content rss-scrollable-content'});
22 | }
23 |
24 | getDisplayText(): string {
25 | return t("RSS_Feeds");
26 | }
27 |
28 | getViewType(): string {
29 | return VIEW_ID;
30 | }
31 |
32 | getIcon(): string {
33 | return "rss";
34 | }
35 |
36 | protected async onOpen(): Promise {
37 | const buttonEl = this.navigationButtonsEl.createDiv('clickable-buttons nav-action-button');
38 | buttonEl.addEventListener('click', async() => {
39 | await this.displayData();
40 | });
41 | setIcon(buttonEl,'refresh-cw');
42 | buttonEl.setAttr('aria-label', t('refresh_feeds'));
43 |
44 | await this.displayData();
45 | }
46 |
47 | private async displayData() {
48 |
49 | this.contentContainer.empty();
50 |
51 | const folders = await this.plugin.providers.getCurrent().folders();
52 |
53 | for (const folder of folders) {
54 | const folderDiv = this.contentContainer.createDiv('rss-folder');
55 | const folderCollapseIcon = folderDiv.createSpan();
56 | setIcon(folderCollapseIcon, 'right-triangle');
57 | folderDiv.createSpan({text: folder.name()});
58 |
59 | for (const feed of folder.feeds()) {
60 | const feedDiv = folderDiv.createDiv('feed');
61 | const feedTitleDiv = feedDiv.createSpan('rss-feed');
62 |
63 | const feedCollapse = feedTitleDiv.createSpan();
64 | setIcon(feedCollapse, 'right-triangle');
65 |
66 | if(feed.favicon()) {
67 | feedTitleDiv.createEl('img', {cls: 'feed-favicon', attr: {src: feed.favicon()}});
68 |
69 | }
70 | feedTitleDiv.createSpan({text: feed.title()});
71 |
72 | const feedList = feedDiv.createEl('ul');
73 |
74 | for (const item of feed.items()) {
75 | const itemDiv = feedList.createEl('li');
76 |
77 | if(item.starred())
78 | setIcon(itemDiv.createSpan(), 'star');
79 | if(item.created())
80 | setIcon(itemDiv.createSpan(), 'document');
81 |
82 | if(item.read())
83 | itemDiv.addClass('rss-read');
84 |
85 | itemDiv.createSpan({text: item.title()});
86 |
87 | itemDiv.onClickEvent(() => {
88 | new ItemModal(this.plugin, item, null, true).open();
89 | });
90 | }
91 | }
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/test/RssParser.test.ts:
--------------------------------------------------------------------------------
1 | import {RssFeed} from "../src/settings/settings";
2 | import {getFeedItems} from "../src/parser/rssParser";
3 |
4 | describe('invalid', () => {
5 | test('not xml', async () => {
6 | const feed: RssFeed = {
7 | name: "Invalid",
8 | url: "./invalid.xml",
9 | folder: ""
10 | };
11 | const result = await getFeedItems(feed);
12 | expect(result).toBeUndefined();
13 |
14 | });
15 | test('Not a RSS feed', async () => {
16 | const feed: RssFeed = {
17 | name: "Invalid",
18 | url: "./example.org.xml",
19 | folder: ""
20 | };
21 | const result = await getFeedItems(feed);
22 | expect(result.items.length).toEqual(0);
23 | expect(result.name).toEqual(feed.name);
24 | expect(result.folder).toEqual(feed.folder);
25 | expect(result.image).toBeNull();
26 |
27 | });
28 | });
29 |
30 | describe('Wallabag', () => {
31 | test('live', async () => {
32 | const feed: RssFeed = {
33 | name: "Wallabag",
34 | url: "https://wallabag.joethei.de/feed/testUser/vPKtC7bLgxvUmkF/all",
35 | folder: ""
36 | };
37 | const result = await getFeedItems(feed);
38 | expect(result.items.length).toEqual(3);
39 | expect(result.title).toEqual("wallabag — all feed");
40 | expect(result.image).toEqual("https://wallabag.joethei.de/favicon.ico");
41 | expect(result.items[0].title).toEqual("Using Obsidian For Writing Fiction & Notes » Eleanor Konik");
42 |
43 | });
44 | test('fake', async () => {
45 | const feed: RssFeed = {
46 | name: "Wallabag",
47 | url: "./wallabag.xml",
48 | folder: ""
49 | };
50 |
51 | const result = await getFeedItems(feed);
52 | expect(result.items.length).toEqual(3);
53 | })
54 | });
55 |
--------------------------------------------------------------------------------
/test/importData.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Nachrichten aus der Politik
4 |
5 |
6 |
8 |
11 |
14 |
17 |
20 |
23 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/test/locale.test.ts:
--------------------------------------------------------------------------------
1 | import t from "../src/l10n/locale";
2 |
3 | describe('translation', function () {
4 | test('', function () {
5 | expect(t("testingValue")).toEqual("Hello World");
6 | });
7 | test('fallback to default if no value in selected language', function () {
8 | expect(t("save")).toEqual("Save");
9 | });
10 | test('inserts', function () {
11 | expect(t('testingInserts', "World", "!")).toEqual("Hello World !");
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/test/subfolders.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Obsidian RSS Export
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
22 |
24 |
26 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/test/subscriptions.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/test/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "baseUrl": "./",
5 | "module": "commonjs",
6 | "experimentalDecorators": true,
7 | "strictPropertyInitialization": false,
8 | "isolatedModules": false,
9 | "strict": false,
10 | "noImplicitAny": false,
11 | "types": [
12 | "jest"
13 | ],
14 | "typeRoots" : [
15 | "../node_modules/@types"
16 | ]
17 | },
18 | "exclude": [
19 | "../node_modules"
20 | ],
21 | "include": [
22 | "./**/*.ts"
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "experimentalDecorators": true,
4 | "baseUrl": ".",
5 | "inlineSourceMap": true,
6 | "inlineSources": true,
7 | "module": "ESNext",
8 | "target": "es2018",
9 | "allowJs": true,
10 | "noImplicitAny": true,
11 | "moduleResolution": "node",
12 | "importHelpers": true,
13 | "isolatedModules": false,
14 | "types": [
15 | "node",
16 | "svelte",
17 | "jest"
18 | ],
19 | "lib": [
20 | "dom",
21 | "es5",
22 | "scripthost",
23 | "es2015"
24 | ],
25 | "allowSyntheticDefaultImports": true
26 | },
27 | }
28 |
--------------------------------------------------------------------------------
/versions.json:
--------------------------------------------------------------------------------
1 | {
2 | "0.1.0": "0.9.12",
3 | "0.2.0": "0.9.12",
4 | "0.3.0": "0.9.12",
5 | "0.3.1": "0.9.12",
6 | "0.4.0": "0.9.12",
7 | "0.5.0": "0.9.12",
8 | "0.6.0": "0.9.12",
9 | "0.6.1": "0.9.12",
10 | "0.6.2": "0.9.12",
11 | "0.6.3": "0.9.12",
12 | "0.6.4": "0.9.12",
13 | "0.6.5": "0.9.12",
14 | "0.6.6": "0.9.12",
15 | "0.7.0": "0.9.12",
16 | "0.7.1": "0.9.12",
17 | "0.8.0": "0.12.19",
18 | "0.9.0": "0.12.19",
19 | "0.9.1": "0.12.19",
20 | "0.9.2": "0.13.14",
21 | "0.9.3": "0.12.17",
22 | "1.0.0": "0.12.17",
23 | "1.0.1": "0.12.17",
24 | "1.0.2": "0.12.17",
25 | "1.0.3": "0.12.17",
26 | "1.0.4": "0.12.17",
27 | "1.0.5": "0.12.17",
28 | "1.1.0": "0.12.17",
29 | "1.2.0": "0.13.33",
30 | "1.2.1": "0.13.33",
31 | "1.2.2": "0.13.33"
32 | }
33 |
--------------------------------------------------------------------------------