├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.yml │ └── bug_report.yml ├── workflows │ ├── linter.yaml │ ├── translator.yml │ ├── close-stale-issues.yml │ ├── dockerhub-description.yml │ ├── scheduled-docker-build.yml │ ├── scheduled-tag.yml │ ├── deploy-gh-pages.yml │ ├── deploy-docker.yml │ ├── dependabot-auto-merge.yml │ └── update-fonts.yml └── dependabot.yml ├── images ├── devices.png └── screenshot.png ├── public ├── favicon.ico ├── logo192.png ├── logo512.png ├── robots.txt ├── default-feed-icon.png ├── fonts │ ├── fira-sans │ │ ├── va9E4kDNxMZdWfMOD5VfkA.ttf │ │ ├── va9E4kDNxMZdWfMOD5Vvl4jL.woff2 │ │ ├── va9E4kDNxMZdWfMOD5Vvk4jLeTY.woff2 │ │ ├── va9E4kDNxMZdWfMOD5VvlIjLeTY.woff2 │ │ ├── va9E4kDNxMZdWfMOD5Vvm4jLeTY.woff2 │ │ ├── va9E4kDNxMZdWfMOD5VvmIjLeTY.woff2 │ │ ├── va9E4kDNxMZdWfMOD5VvmYjLeTY.woff2 │ │ └── va9E4kDNxMZdWfMOD5VvmojLeTY.woff2 │ ├── source-sans-pro │ │ ├── 6xK3dSBYKcSV-LCoeQqfX1RYOo3aPw.ttf │ │ ├── 6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7l.woff2 │ │ ├── 6xK3dSBYKcSV-LCoeQqfX1RYOo3qN67lqDY.woff2 │ │ ├── 6xK3dSBYKcSV-LCoeQqfX1RYOo3qNK7lqDY.woff2 │ │ ├── 6xK3dSBYKcSV-LCoeQqfX1RYOo3qNa7lqDY.woff2 │ │ ├── 6xK3dSBYKcSV-LCoeQqfX1RYOo3qNq7lqDY.woff2 │ │ ├── 6xK3dSBYKcSV-LCoeQqfX1RYOo3qO67lqDY.woff2 │ │ └── 6xK3dSBYKcSV-LCoeQqfX1RYOo3qPK7lqDY.woff2 │ ├── source-serif-pro │ │ ├── neIQzD-0qpwxpaWvjeD0X88SAOeaiXM.ttf │ │ ├── neIQzD-0qpwxpaWvjeD0X88SAOeauXQ-oA.woff2 │ │ ├── neIQzD-0qpwxpaWvjeD0X88SAOeauXA-oBOL.woff2 │ │ ├── neIQzD-0qpwxpaWvjeD0X88SAOeauXc-oBOL.woff2 │ │ ├── neIQzD-0qpwxpaWvjeD0X88SAOeauXk-oBOL.woff2 │ │ ├── neIQzD-0qpwxpaWvjeD0X88SAOeauXo-oBOL.woff2 │ │ └── neIQzD-0qpwxpaWvjeD0X88SAOeauXs-oBOL.woff2 │ ├── noto-sans │ │ ├── o-0mIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A99d.ttf │ │ ├── o-0mIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A-9a6VI.woff2 │ │ ├── o-0mIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A-9U6VLKzA.woff2 │ │ ├── o-0mIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A-9V6VLKzA.woff2 │ │ ├── o-0mIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A-9W6VLKzA.woff2 │ │ ├── o-0mIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A-9X6VLKzA.woff2 │ │ ├── o-0mIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A-9Z6VLKzA.woff2 │ │ ├── o-0mIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A-9b6VLKzA.woff2 │ │ └── o-0mIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A-9e6VLKzA.woff2 │ ├── open-sans │ │ ├── memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0C4n.ttf │ │ ├── memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4gaVI.woff2 │ │ ├── memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4iaVIGxA.woff2 │ │ ├── memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4jaVIGxA.woff2 │ │ ├── memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4kaVIGxA.woff2 │ │ ├── memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4saVIGxA.woff2 │ │ ├── memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4taVIGxA.woff2 │ │ ├── memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4uaVIGxA.woff2 │ │ ├── memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4vaVIGxA.woff2 │ │ ├── memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B5OaVIGxA.woff2 │ │ └── memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B5caVIGxA.woff2 │ └── noto-serif │ │ ├── ga6iaw1J5X9T9RW6j9bNVls-hfgvz8JcMofYTa32J4wsL2JAlAhZqFCjwA.ttf │ │ ├── ga6iaw1J5X9T9RW6j9bNVls-hfgvz8JcMofYTa32J4wsL2JAlAhZqFCTx8cK.woff2 │ │ ├── ga6iaw1J5X9T9RW6j9bNVls-hfgvz8JcMofYTa32J4wsL2JAlAhZqFCTu8cKtq8.woff2 │ │ ├── ga6iaw1J5X9T9RW6j9bNVls-hfgvz8JcMofYTa32J4wsL2JAlAhZqFCTw8cKtq8.woff2 │ │ ├── ga6iaw1J5X9T9RW6j9bNVls-hfgvz8JcMofYTa32J4wsL2JAlAhZqFCTxMcKtq8.woff2 │ │ ├── ga6iaw1J5X9T9RW6j9bNVls-hfgvz8JcMofYTa32J4wsL2JAlAhZqFCTy8cKtq8.woff2 │ │ ├── ga6iaw1J5X9T9RW6j9bNVls-hfgvz8JcMofYTa32J4wsL2JAlAhZqFCTyMcKtq8.woff2 │ │ ├── ga6iaw1J5X9T9RW6j9bNVls-hfgvz8JcMofYTa32J4wsL2JAlAhZqFCTyccKtq8.woff2 │ │ └── ga6iaw1J5X9T9RW6j9bNVls-hfgvz8JcMofYTa32J4wsL2JAlAhZqFCTyscKtq8.woff2 ├── manifest.json └── styles │ └── loading.css ├── src ├── components │ ├── Settings │ │ ├── Appearance.css │ │ ├── SettingItem.css │ │ ├── SettingsTabs.css │ │ ├── CategoryList.css │ │ ├── SettingItem.jsx │ │ ├── FeedList.css │ │ ├── Hotkeys.jsx │ │ ├── CategoryList.jsx │ │ ├── EditableTag.jsx │ │ ├── SettingsTabs.jsx │ │ └── EditableTagGroup.jsx │ ├── Article │ │ ├── ImageLinkTag.css │ │ ├── LoadingCards.css │ │ ├── SidebarTrigger.css │ │ ├── CodeBlock.css │ │ ├── ArticleList.css │ │ ├── ImageOverlayButton.css │ │ ├── ImageLinkTag.jsx │ │ ├── ArticleTOC.css │ │ ├── LoadingCards.jsx │ │ ├── ActionButtons.css │ │ ├── SearchAndSortBar.css │ │ ├── SidebarTrigger.jsx │ │ ├── ArticleTOC.jsx │ │ ├── CodeBlock.jsx │ │ ├── ArticleCard.css │ │ ├── ArticleList.jsx │ │ └── ImageOverlayButton.jsx │ ├── Content │ │ ├── FooterPanel.css │ │ ├── Content.css │ │ └── ContentContext.jsx │ ├── ui │ │ ├── Ripple.css │ │ ├── PlyrPlayer.css │ │ ├── FadeTransition.jsx │ │ ├── CustomTooltip.jsx │ │ ├── CustomLink.jsx │ │ ├── Ripple.jsx │ │ ├── EditCategoryModal.jsx │ │ └── FeedIcon.jsx │ ├── Sidebar │ │ ├── Profile.css │ │ ├── AddFeed.jsx │ │ ├── Sidebar.css │ │ └── Profile.jsx │ └── Main │ │ └── Main.css ├── pages │ ├── images │ │ └── background.jpg │ ├── History.jsx │ ├── Starred.jsx │ ├── All.jsx │ ├── RouterProtect.jsx │ ├── Feed.jsx │ ├── Category.jsx │ ├── Login.css │ ├── ErrorPage.jsx │ └── Today.jsx ├── utils │ ├── time.js │ ├── loading.js │ ├── auth.js │ ├── constants.js │ ├── filter.js │ ├── version.js │ ├── nanostores.js │ ├── form.js │ ├── dom.js │ ├── url.js │ ├── locales.js │ ├── colors.js │ ├── deduplicate.js │ ├── images.js │ ├── date.js │ └── highlighter.js ├── hooks │ ├── useContentContext.js │ ├── usePhotoSlider.js │ ├── useScreenWidth.js │ ├── useFeedIconsSync.js │ ├── useModalToggle.js │ ├── useFeedIcons.js │ ├── useTheme.js │ ├── useDocumentTitle.js │ ├── useVersionCheck.js │ ├── useLanguage.js │ ├── useContentHotkeys.js │ ├── useArticleList.js │ ├── useCategoryOperations.js │ └── useAppData.js ├── store │ ├── sidebarState.js │ ├── feedIconsState.js │ ├── authState.js │ ├── settingsState.js │ ├── hotkeysState.js │ └── dataState.js ├── scripts │ └── version-info.js ├── main.jsx ├── index.css ├── apis │ ├── index.js │ ├── categories.js │ ├── feeds.js │ └── ofetch.js ├── routes.jsx ├── theme.css ├── App.css └── App.jsx ├── docker-compose.yml ├── jsconfig.json ├── Caddyfile ├── .editorconfig ├── .prettierrc ├── .prettierignore ├── .cta.json ├── .gitignore ├── Dockerfile ├── LICENSE ├── vite.config.js ├── index.html ├── package.json ├── docs └── README.zh-CN.md └── eslint.config.mjs /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /images/devices.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/images/devices.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/components/Settings/Appearance.css: -------------------------------------------------------------------------------- 1 | .arco-radio { 2 | padding-left: 0; 3 | } 4 | -------------------------------------------------------------------------------- /images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/images/screenshot.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/default-feed-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/default-feed-icon.png -------------------------------------------------------------------------------- /src/pages/images/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/src/pages/images/background.jpg -------------------------------------------------------------------------------- /src/components/Article/ImageLinkTag.css: -------------------------------------------------------------------------------- 1 | .link-tag { 2 | cursor: pointer; 3 | max-width: calc(100% - 32px); 4 | } 5 | -------------------------------------------------------------------------------- /src/utils/time.js: -------------------------------------------------------------------------------- 1 | const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) 2 | 3 | export default sleep 4 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | reactflux: 3 | image: electh/reactflux 4 | restart: always 5 | ports: 6 | - 2000:2000 7 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Caddyfile: -------------------------------------------------------------------------------- 1 | :2000 2 | 3 | root * /srv 4 | 5 | try_files {path} {path}/ /index.html 6 | 7 | # Enable the static file server. 8 | file_server 9 | -------------------------------------------------------------------------------- /public/fonts/fira-sans/va9E4kDNxMZdWfMOD5VfkA.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/fira-sans/va9E4kDNxMZdWfMOD5VfkA.ttf -------------------------------------------------------------------------------- /src/components/Settings/SettingItem.css: -------------------------------------------------------------------------------- 1 | .setting-row { 2 | display: flex; 3 | justify-content: space-between; 4 | align-items: center; 5 | } 6 | -------------------------------------------------------------------------------- /public/fonts/fira-sans/va9E4kDNxMZdWfMOD5Vvl4jL.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/fira-sans/va9E4kDNxMZdWfMOD5Vvl4jL.woff2 -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 2 8 | max_line_length = 100 9 | -------------------------------------------------------------------------------- /public/fonts/fira-sans/va9E4kDNxMZdWfMOD5Vvk4jLeTY.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/fira-sans/va9E4kDNxMZdWfMOD5Vvk4jLeTY.woff2 -------------------------------------------------------------------------------- /public/fonts/fira-sans/va9E4kDNxMZdWfMOD5VvlIjLeTY.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/fira-sans/va9E4kDNxMZdWfMOD5VvlIjLeTY.woff2 -------------------------------------------------------------------------------- /public/fonts/fira-sans/va9E4kDNxMZdWfMOD5Vvm4jLeTY.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/fira-sans/va9E4kDNxMZdWfMOD5Vvm4jLeTY.woff2 -------------------------------------------------------------------------------- /public/fonts/fira-sans/va9E4kDNxMZdWfMOD5VvmIjLeTY.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/fira-sans/va9E4kDNxMZdWfMOD5VvmIjLeTY.woff2 -------------------------------------------------------------------------------- /public/fonts/fira-sans/va9E4kDNxMZdWfMOD5VvmYjLeTY.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/fira-sans/va9E4kDNxMZdWfMOD5VvmYjLeTY.woff2 -------------------------------------------------------------------------------- /public/fonts/fira-sans/va9E4kDNxMZdWfMOD5VvmojLeTY.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/fira-sans/va9E4kDNxMZdWfMOD5VvmojLeTY.woff2 -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "printWidth": 100, 4 | "semi": false, 5 | "singleQuote": false, 6 | "tabWidth": 2, 7 | "trailingComma": "all" 8 | } 9 | -------------------------------------------------------------------------------- /public/fonts/source-sans-pro/6xK3dSBYKcSV-LCoeQqfX1RYOo3aPw.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/source-sans-pro/6xK3dSBYKcSV-LCoeQqfX1RYOo3aPw.ttf -------------------------------------------------------------------------------- /public/fonts/source-serif-pro/neIQzD-0qpwxpaWvjeD0X88SAOeaiXM.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/source-serif-pro/neIQzD-0qpwxpaWvjeD0X88SAOeaiXM.ttf -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | package-lock.json 3 | pnpm-lock.yaml 4 | yarn.lock 5 | public/styles/fonts.css 6 | src/components/ui/*.tsx 7 | src/hooks/use-mobile.tsx 8 | src/lib/utils.ts 9 | -------------------------------------------------------------------------------- /public/fonts/source-sans-pro/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7l.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/source-sans-pro/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7l.woff2 -------------------------------------------------------------------------------- /public/fonts/source-sans-pro/6xK3dSBYKcSV-LCoeQqfX1RYOo3qN67lqDY.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/source-sans-pro/6xK3dSBYKcSV-LCoeQqfX1RYOo3qN67lqDY.woff2 -------------------------------------------------------------------------------- /public/fonts/source-sans-pro/6xK3dSBYKcSV-LCoeQqfX1RYOo3qNK7lqDY.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/source-sans-pro/6xK3dSBYKcSV-LCoeQqfX1RYOo3qNK7lqDY.woff2 -------------------------------------------------------------------------------- /public/fonts/source-sans-pro/6xK3dSBYKcSV-LCoeQqfX1RYOo3qNa7lqDY.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/source-sans-pro/6xK3dSBYKcSV-LCoeQqfX1RYOo3qNa7lqDY.woff2 -------------------------------------------------------------------------------- /public/fonts/source-sans-pro/6xK3dSBYKcSV-LCoeQqfX1RYOo3qNq7lqDY.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/source-sans-pro/6xK3dSBYKcSV-LCoeQqfX1RYOo3qNq7lqDY.woff2 -------------------------------------------------------------------------------- /public/fonts/source-sans-pro/6xK3dSBYKcSV-LCoeQqfX1RYOo3qO67lqDY.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/source-sans-pro/6xK3dSBYKcSV-LCoeQqfX1RYOo3qO67lqDY.woff2 -------------------------------------------------------------------------------- /public/fonts/source-sans-pro/6xK3dSBYKcSV-LCoeQqfX1RYOo3qPK7lqDY.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/source-sans-pro/6xK3dSBYKcSV-LCoeQqfX1RYOo3qPK7lqDY.woff2 -------------------------------------------------------------------------------- /public/fonts/source-serif-pro/neIQzD-0qpwxpaWvjeD0X88SAOeauXQ-oA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/source-serif-pro/neIQzD-0qpwxpaWvjeD0X88SAOeauXQ-oA.woff2 -------------------------------------------------------------------------------- /public/fonts/source-serif-pro/neIQzD-0qpwxpaWvjeD0X88SAOeauXA-oBOL.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/source-serif-pro/neIQzD-0qpwxpaWvjeD0X88SAOeauXA-oBOL.woff2 -------------------------------------------------------------------------------- /public/fonts/source-serif-pro/neIQzD-0qpwxpaWvjeD0X88SAOeauXc-oBOL.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/source-serif-pro/neIQzD-0qpwxpaWvjeD0X88SAOeauXc-oBOL.woff2 -------------------------------------------------------------------------------- /public/fonts/source-serif-pro/neIQzD-0qpwxpaWvjeD0X88SAOeauXk-oBOL.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/source-serif-pro/neIQzD-0qpwxpaWvjeD0X88SAOeauXk-oBOL.woff2 -------------------------------------------------------------------------------- /public/fonts/source-serif-pro/neIQzD-0qpwxpaWvjeD0X88SAOeauXo-oBOL.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/source-serif-pro/neIQzD-0qpwxpaWvjeD0X88SAOeauXo-oBOL.woff2 -------------------------------------------------------------------------------- /public/fonts/source-serif-pro/neIQzD-0qpwxpaWvjeD0X88SAOeauXs-oBOL.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/source-serif-pro/neIQzD-0qpwxpaWvjeD0X88SAOeauXs-oBOL.woff2 -------------------------------------------------------------------------------- /src/components/Article/LoadingCards.css: -------------------------------------------------------------------------------- 1 | .card-cover-style { 2 | aspect-ratio: 16 / 9; 3 | width: 100%; 4 | } 5 | 6 | .card-style { 7 | margin-bottom: 10px; 8 | width: 100%; 9 | } 10 | -------------------------------------------------------------------------------- /public/fonts/noto-sans/o-0mIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A99d.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/noto-sans/o-0mIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A99d.ttf -------------------------------------------------------------------------------- /public/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0C4n.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0C4n.ttf -------------------------------------------------------------------------------- /src/components/Settings/SettingsTabs.css: -------------------------------------------------------------------------------- 1 | .custom-tabs .arco-tabs-header { 2 | margin: 0 auto !important; 3 | } 4 | 5 | .input-select, 6 | .input-slider { 7 | margin-left: 16px; 8 | width: 130px; 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/loading.js: -------------------------------------------------------------------------------- 1 | const hideSpinner = () => { 2 | const spinner = document.querySelector(".spinner") 3 | if (spinner) { 4 | spinner.style.display = "none" 5 | } 6 | } 7 | 8 | export default hideSpinner 9 | -------------------------------------------------------------------------------- /public/fonts/noto-sans/o-0mIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A-9a6VI.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/noto-sans/o-0mIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A-9a6VI.woff2 -------------------------------------------------------------------------------- /public/fonts/noto-serif/ga6iaw1J5X9T9RW6j9bNVls-hfgvz8JcMofYTa32J4wsL2JAlAhZqFCjwA.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/noto-serif/ga6iaw1J5X9T9RW6j9bNVls-hfgvz8JcMofYTa32J4wsL2JAlAhZqFCjwA.ttf -------------------------------------------------------------------------------- /public/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4gaVI.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4gaVI.woff2 -------------------------------------------------------------------------------- /public/fonts/noto-serif/ga6iaw1J5X9T9RW6j9bNVls-hfgvz8JcMofYTa32J4wsL2JAlAhZqFCTx8cK.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/noto-serif/ga6iaw1J5X9T9RW6j9bNVls-hfgvz8JcMofYTa32J4wsL2JAlAhZqFCTx8cK.woff2 -------------------------------------------------------------------------------- /public/fonts/noto-sans/o-0mIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A-9U6VLKzA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/noto-sans/o-0mIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A-9U6VLKzA.woff2 -------------------------------------------------------------------------------- /public/fonts/noto-sans/o-0mIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A-9V6VLKzA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/noto-sans/o-0mIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A-9V6VLKzA.woff2 -------------------------------------------------------------------------------- /public/fonts/noto-sans/o-0mIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A-9W6VLKzA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/noto-sans/o-0mIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A-9W6VLKzA.woff2 -------------------------------------------------------------------------------- /public/fonts/noto-sans/o-0mIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A-9X6VLKzA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/noto-sans/o-0mIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A-9X6VLKzA.woff2 -------------------------------------------------------------------------------- /public/fonts/noto-sans/o-0mIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A-9Z6VLKzA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/noto-sans/o-0mIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A-9Z6VLKzA.woff2 -------------------------------------------------------------------------------- /public/fonts/noto-sans/o-0mIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A-9b6VLKzA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/noto-sans/o-0mIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A-9b6VLKzA.woff2 -------------------------------------------------------------------------------- /public/fonts/noto-sans/o-0mIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A-9e6VLKzA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/noto-sans/o-0mIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjcz6L1SoM-jCpoiyD9A-9e6VLKzA.woff2 -------------------------------------------------------------------------------- /public/fonts/noto-serif/ga6iaw1J5X9T9RW6j9bNVls-hfgvz8JcMofYTa32J4wsL2JAlAhZqFCTu8cKtq8.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/noto-serif/ga6iaw1J5X9T9RW6j9bNVls-hfgvz8JcMofYTa32J4wsL2JAlAhZqFCTu8cKtq8.woff2 -------------------------------------------------------------------------------- /public/fonts/noto-serif/ga6iaw1J5X9T9RW6j9bNVls-hfgvz8JcMofYTa32J4wsL2JAlAhZqFCTw8cKtq8.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/noto-serif/ga6iaw1J5X9T9RW6j9bNVls-hfgvz8JcMofYTa32J4wsL2JAlAhZqFCTw8cKtq8.woff2 -------------------------------------------------------------------------------- /public/fonts/noto-serif/ga6iaw1J5X9T9RW6j9bNVls-hfgvz8JcMofYTa32J4wsL2JAlAhZqFCTxMcKtq8.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/noto-serif/ga6iaw1J5X9T9RW6j9bNVls-hfgvz8JcMofYTa32J4wsL2JAlAhZqFCTxMcKtq8.woff2 -------------------------------------------------------------------------------- /public/fonts/noto-serif/ga6iaw1J5X9T9RW6j9bNVls-hfgvz8JcMofYTa32J4wsL2JAlAhZqFCTy8cKtq8.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/noto-serif/ga6iaw1J5X9T9RW6j9bNVls-hfgvz8JcMofYTa32J4wsL2JAlAhZqFCTy8cKtq8.woff2 -------------------------------------------------------------------------------- /public/fonts/noto-serif/ga6iaw1J5X9T9RW6j9bNVls-hfgvz8JcMofYTa32J4wsL2JAlAhZqFCTyMcKtq8.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/noto-serif/ga6iaw1J5X9T9RW6j9bNVls-hfgvz8JcMofYTa32J4wsL2JAlAhZqFCTyMcKtq8.woff2 -------------------------------------------------------------------------------- /public/fonts/noto-serif/ga6iaw1J5X9T9RW6j9bNVls-hfgvz8JcMofYTa32J4wsL2JAlAhZqFCTyccKtq8.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/noto-serif/ga6iaw1J5X9T9RW6j9bNVls-hfgvz8JcMofYTa32J4wsL2JAlAhZqFCTyccKtq8.woff2 -------------------------------------------------------------------------------- /public/fonts/noto-serif/ga6iaw1J5X9T9RW6j9bNVls-hfgvz8JcMofYTa32J4wsL2JAlAhZqFCTyscKtq8.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/noto-serif/ga6iaw1J5X9T9RW6j9bNVls-hfgvz8JcMofYTa32J4wsL2JAlAhZqFCTyscKtq8.woff2 -------------------------------------------------------------------------------- /public/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4iaVIGxA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4iaVIGxA.woff2 -------------------------------------------------------------------------------- /public/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4jaVIGxA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4jaVIGxA.woff2 -------------------------------------------------------------------------------- /public/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4kaVIGxA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4kaVIGxA.woff2 -------------------------------------------------------------------------------- /public/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4saVIGxA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4saVIGxA.woff2 -------------------------------------------------------------------------------- /public/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4taVIGxA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4taVIGxA.woff2 -------------------------------------------------------------------------------- /public/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4uaVIGxA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4uaVIGxA.woff2 -------------------------------------------------------------------------------- /public/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4vaVIGxA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4vaVIGxA.woff2 -------------------------------------------------------------------------------- /public/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B5OaVIGxA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B5OaVIGxA.woff2 -------------------------------------------------------------------------------- /public/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B5caVIGxA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electh/ReactFlux/HEAD/public/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B5caVIGxA.woff2 -------------------------------------------------------------------------------- /src/hooks/useContentContext.js: -------------------------------------------------------------------------------- 1 | import { useContext } from "react" 2 | 3 | import { ContentContext } from "@/components/Content/ContentContext" 4 | 5 | const useContentContext = () => useContext(ContentContext) 6 | 7 | export default useContentContext 8 | -------------------------------------------------------------------------------- /src/components/Content/FooterPanel.css: -------------------------------------------------------------------------------- 1 | .entry-panel { 2 | display: flex; 3 | flex-direction: row; 4 | justify-content: space-between; 5 | z-index: 2; 6 | padding: 8px 10px; 7 | } 8 | 9 | .entry-panel .arco-btn-secondary { 10 | background-color: transparent; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Article/SidebarTrigger.css: -------------------------------------------------------------------------------- 1 | .brand { 2 | display: flex; 3 | align-items: center; 4 | margin-left: 10px; 5 | } 6 | 7 | .sidebar-drawer .arco-drawer-content { 8 | padding: 0; 9 | background-color: var(--color-neutral-2); 10 | } 11 | 12 | .trigger { 13 | display: none; 14 | } 15 | -------------------------------------------------------------------------------- /.cta.json: -------------------------------------------------------------------------------- 1 | { 2 | "framework": "react", 3 | "git": true, 4 | "mode": "code-router", 5 | "packageManager": "pnpm", 6 | "projectName": "ReactFlux", 7 | "tailwind": false, 8 | "toolchain": "eslint+prettier", 9 | "typescript": false, 10 | "variableValues": {}, 11 | "version": 1, 12 | "existingAddOns": [] 13 | } 14 | -------------------------------------------------------------------------------- /src/pages/History.jsx: -------------------------------------------------------------------------------- 1 | import { getHistoryEntries } from "@/apis" 2 | import Content from "@/components/Content/Content" 3 | 4 | const getEntries = (_status, _starred, filterParams) => getHistoryEntries(filterParams) 5 | 6 | const History = () => 7 | 8 | export default History 9 | -------------------------------------------------------------------------------- /src/store/sidebarState.js: -------------------------------------------------------------------------------- 1 | import { persistentAtom } from "@nanostores/persistent" 2 | 3 | export const expandedCategoriesState = persistentAtom("expandedCategories", [], { 4 | encode: JSON.stringify, 5 | decode: JSON.parse, 6 | }) 7 | 8 | export const setExpandedCategories = (keys) => { 9 | expandedCategoriesState.set(keys) 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/auth.js: -------------------------------------------------------------------------------- 1 | import isURL from "validator/lib/isURL" 2 | 3 | const isValidAuth = (auth) => { 4 | const { server, token, username, password } = auth 5 | if (!server || !isURL(server, { require_protocol: true })) { 6 | return false 7 | } 8 | return token || (username && password) 9 | } 10 | 11 | export default isValidAuth 12 | -------------------------------------------------------------------------------- /src/pages/Starred.jsx: -------------------------------------------------------------------------------- 1 | import { getStarredEntries } from "@/apis" 2 | import Content from "@/components/Content/Content" 3 | 4 | const getEntries = (status, _starred, filterParams) => getStarredEntries(status, filterParams) 5 | 6 | const Starred = () => 7 | 8 | export default Starred 9 | -------------------------------------------------------------------------------- /src/utils/constants.js: -------------------------------------------------------------------------------- 1 | export const ANIMATION_DURATION_S = 0.25 2 | export const ANIMATION_DURATION_MS = ANIMATION_DURATION_S * 1000 3 | 4 | export const GITHUB_REPO_PATH = "electh/ReactFlux" 5 | 6 | export const MIN_THUMBNAIL_SIZE = 100 7 | export const WIDE_IMAGE_RATIO = 4 / 3 8 | 9 | export const UPDATE_NOTIFICATION_KEY = "updateNotificationDismissed" 10 | -------------------------------------------------------------------------------- /src/pages/All.jsx: -------------------------------------------------------------------------------- 1 | import { getAllEntries, markAllAsRead } from "@/apis" 2 | import Content from "@/components/Content/Content" 3 | 4 | const getEntries = (status, _starred, filterParams) => getAllEntries(status, filterParams) 5 | 6 | const All = () => ( 7 | 8 | ) 9 | 10 | export default All 11 | -------------------------------------------------------------------------------- /src/store/feedIconsState.js: -------------------------------------------------------------------------------- 1 | import { map } from "nanostores" 2 | 3 | export const defaultIcon = { url: null, width: null, height: null } 4 | export const feedIconsState = map({ 0: defaultIcon }) 5 | 6 | export const updateFeedIcon = (id, feedIconChanges) => 7 | feedIconsState.setKey(id, { ...feedIconsState.get()[id], ...feedIconChanges }) 8 | export const resetFeedIcons = () => feedIconsState.set({ 0: defaultIcon }) 9 | -------------------------------------------------------------------------------- /src/components/ui/Ripple.css: -------------------------------------------------------------------------------- 1 | .ripple-container { 2 | position: absolute; 3 | top: 0; 4 | right: 0; 5 | bottom: 0; 6 | left: 0; 7 | } 8 | 9 | .ripple-span { 10 | position: absolute; 11 | transform: scale(0); 12 | opacity: 0.2; 13 | animation-name: ripple; 14 | border-radius: 100%; 15 | } 16 | 17 | @keyframes ripple { 18 | to { 19 | transform: scale(2); 20 | opacity: 0; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/filter.js: -------------------------------------------------------------------------------- 1 | import { filterByQuery } from "./kmp" 2 | 3 | export const includesIgnoreCase = (text, searchText) => { 4 | return text.toLowerCase().includes(searchText.toLowerCase()) 5 | } 6 | 7 | export const filterEntries = (entries, filterType, filterString) => { 8 | if (!filterString || entries.length === 0) { 9 | return entries 10 | } 11 | return filterByQuery(entries, filterString, [filterType]) 12 | } 13 | -------------------------------------------------------------------------------- /src/components/Settings/CategoryList.css: -------------------------------------------------------------------------------- 1 | .add-category-tag { 2 | cursor: pointer; 3 | border: 1px dashed var(--color-border-2); 4 | background-color: var(--color-bg-1); 5 | width: 32px; 6 | } 7 | 8 | .input-style { 9 | width: 84px; 10 | } 11 | 12 | .modal-style { 13 | width: 400px; 14 | max-width: 95%; 15 | } 16 | 17 | .tag-style { 18 | cursor: pointer; 19 | margin-right: 10px; 20 | margin-bottom: 10px; 21 | } 22 | -------------------------------------------------------------------------------- /src/scripts/version-info.js: -------------------------------------------------------------------------------- 1 | // TODO: Adjust this script to work with the new CI/CD pipeline 2 | import { execSync } from "node:child_process" 3 | import { writeFileSync } from "node:fs" 4 | 5 | const versionInfo = { 6 | gitHash: execSync("git rev-parse --short HEAD").toString().trim(), 7 | gitDate: execSync("git log -1 --format=%cd --date=iso").toString().trim(), 8 | } 9 | 10 | writeFileSync("src/version-info.json", JSON.stringify(versionInfo, null, 2)) 11 | -------------------------------------------------------------------------------- /src/components/Sidebar/Profile.css: -------------------------------------------------------------------------------- 1 | .theme-menu-item { 2 | display: flex; 3 | justify-content: space-between; 4 | align-items: center; 5 | } 6 | 7 | .icon-right { 8 | margin-right: 8px; 9 | font-size: 16px; 10 | transform: translateY(1px); 11 | } 12 | 13 | .arco-dropdown-menu { 14 | max-height: 100%; 15 | } 16 | 17 | .arco-modal-simple .arco-modal-title { 18 | text-align: left; 19 | } 20 | 21 | .arco-modal-simple .arco-modal-footer { 22 | text-align: right; 23 | } 24 | -------------------------------------------------------------------------------- /src/components/Article/CodeBlock.css: -------------------------------------------------------------------------------- 1 | .code-block-container { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | .code-block-container code { 7 | padding: 0 !important; 8 | white-space: pre-wrap !important; 9 | } 10 | 11 | .code-block-header { 12 | display: flex; 13 | justify-content: flex-end; 14 | align-items: center; 15 | gap: 8px; 16 | background: inherit; 17 | z-index: 1; 18 | margin-bottom: -12px; 19 | } 20 | 21 | .language-selector { 22 | width: auto; 23 | } 24 | -------------------------------------------------------------------------------- /src/components/ui/PlyrPlayer.css: -------------------------------------------------------------------------------- 1 | .plyr { 2 | --plyr-color-main: rgb(var(--primary-6)); 3 | } 4 | 5 | .plyr__volume { 6 | position: relative; 7 | } 8 | 9 | .plyr__volume input[data-plyr="volume"] { 10 | display: none; 11 | height: 2rem; 12 | position: absolute; 13 | right: -3rem; 14 | top: -1rem; 15 | transform-origin: left; 16 | transform: rotate(-90deg); 17 | } 18 | 19 | .plyr__volume:hover input[data-plyr="volume"], 20 | .plyr__volume input[data-plyr="volume"]:hover { 21 | display: block; 22 | } 23 | -------------------------------------------------------------------------------- /src/components/Settings/SettingItem.jsx: -------------------------------------------------------------------------------- 1 | import { Typography } from "@arco-design/web-react" 2 | 3 | import "./SettingItem.css" 4 | 5 | const SettingItem = ({ title, description, children }) => ( 6 |
7 |
8 | 9 | {title} 10 | 11 | {description} 12 |
13 | {children} 14 |
15 | ) 16 | 17 | export default SettingItem 18 | -------------------------------------------------------------------------------- /src/components/ui/FadeTransition.jsx: -------------------------------------------------------------------------------- 1 | import { motion } from "framer-motion" 2 | 3 | import { ANIMATION_DURATION_S } from "@/utils/constants" 4 | 5 | const FadeTransition = ({ children, duration = ANIMATION_DURATION_S, y = 0, x = 0, ...props }) => ( 6 | 13 | {children} 14 | 15 | ) 16 | 17 | export default FadeTransition 18 | -------------------------------------------------------------------------------- /src/pages/RouterProtect.jsx: -------------------------------------------------------------------------------- 1 | import { useStore } from "@nanostores/react" 2 | import { Navigate, Outlet, useLocation } from "react-router" 3 | 4 | import { authState } from "@/store/authState" 5 | import isValidAuth from "@/utils/auth" 6 | 7 | const RouterProtect = () => { 8 | const auth = useStore(authState) 9 | const location = useLocation() 10 | 11 | if (isValidAuth(auth)) { 12 | return 13 | } 14 | return 15 | } 16 | 17 | export default RouterProtect 18 | -------------------------------------------------------------------------------- /.github/workflows/linter.yaml: -------------------------------------------------------------------------------- 1 | name: Linter 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | jobs: 8 | lint: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v6 13 | 14 | - name: Install pnpm 15 | run: npm install -g pnpm 16 | 17 | - name: Install dependencies 18 | run: pnpm install --frozen-lockfile 19 | 20 | - name: Run ESLint check 21 | run: pnpm lint 22 | 23 | - name: Run Prettier check 24 | run: pnpm format:check 25 | -------------------------------------------------------------------------------- /src/store/authState.js: -------------------------------------------------------------------------------- 1 | import { persistentAtom } from "@nanostores/persistent" 2 | 3 | const defaultValue = { 4 | server: "", 5 | token: "", 6 | username: "", 7 | password: "", 8 | } 9 | 10 | export const authState = persistentAtom("auth", defaultValue, { 11 | encode: JSON.stringify, 12 | decode: (str) => { 13 | const storedValue = JSON.parse(str) 14 | return { ...defaultValue, ...storedValue } 15 | }, 16 | }) 17 | 18 | export const setAuth = (authChanges) => authState.set(authChanges) 19 | 20 | export const resetAuth = () => setAuth(defaultValue) 21 | -------------------------------------------------------------------------------- /src/utils/version.js: -------------------------------------------------------------------------------- 1 | const compareVersions = (version1, version2) => { 2 | const versionParts1 = version1.split(".") 3 | const versionParts2 = version2.split(".") 4 | 5 | for (let index = 0; index < Math.max(versionParts1.length, versionParts2.length); index++) { 6 | const part1 = Number.parseInt(versionParts1[index] || "0", 10) 7 | const part2 = Number.parseInt(versionParts2[index] || "0", 10) 8 | 9 | if (part1 !== part2) { 10 | return part1 < part2 ? -1 : 1 11 | } 12 | } 13 | 14 | return 0 15 | } 16 | 17 | export default compareVersions 18 | -------------------------------------------------------------------------------- /src/pages/Feed.jsx: -------------------------------------------------------------------------------- 1 | import { partial } from "lodash-es" 2 | import { useParams } from "react-router" 3 | 4 | import { getFeedEntries, markFeedAsRead } from "@/apis" 5 | import Content from "@/components/Content/Content" 6 | 7 | const Feed = () => { 8 | const { id: feedId } = useParams() 9 | 10 | const getEntries = partial(getFeedEntries, feedId) 11 | 12 | return ( 13 | markFeedAsRead(feedId)} 17 | /> 18 | ) 19 | } 20 | 21 | export default Feed 22 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import "@arco-design/web-react/dist/css/arco.css" 2 | import ReactDOM from "react-dom/client" 3 | import { RouterProvider } from "react-router/dom" 4 | import { registerSW } from "virtual:pwa-register" 5 | 6 | import "simplebar-react/dist/simplebar.min.css" 7 | 8 | import "./index.css" 9 | import router from "./routes" 10 | import { registerLanguages } from "./utils/highlighter" 11 | import "./theme.css" 12 | 13 | registerSW({ immediate: true }) 14 | registerLanguages() 15 | 16 | ReactDOM.createRoot(document.querySelector("#root")).render() 17 | -------------------------------------------------------------------------------- /src/components/Article/ArticleList.css: -------------------------------------------------------------------------------- 1 | .entry-list { 2 | flex: 1; 3 | contain: strict; 4 | padding: 10px; 5 | height: 100%; 6 | overflow-x: hidden; 7 | overscroll-behavior-y: contain; 8 | } 9 | 10 | .load-more-container { 11 | display: flex; 12 | justify-content: center; 13 | align-items: center; 14 | margin: 10px auto; 15 | border-radius: var(--border-radius-medium); 16 | background-color: var(--color-bg-2); 17 | color: var(--color-text-2); 18 | } 19 | 20 | @media screen and (max-width: 768px) { 21 | .entry-list { 22 | width: calc(100% - 20px); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/ui/CustomTooltip.jsx: -------------------------------------------------------------------------------- 1 | import { Tooltip } from "@arco-design/web-react" 2 | import { useState } from "react" 3 | 4 | import useScreenWidth from "@/hooks/useScreenWidth" 5 | 6 | const CustomTooltip = ({ children, ...props }) => { 7 | const { isBelowMedium } = useScreenWidth() 8 | const [isHovered, setIsHovered] = useState(false) 9 | 10 | return ( 11 | setIsHovered(visible)} 14 | {...props} 15 | > 16 | {children} 17 | 18 | ) 19 | } 20 | 21 | export default CustomTooltip 22 | -------------------------------------------------------------------------------- /src/pages/Category.jsx: -------------------------------------------------------------------------------- 1 | import { partial } from "lodash-es" 2 | import { useParams } from "react-router" 3 | 4 | import { getCategoryEntries, markCategoryAsRead } from "@/apis" 5 | import Content from "@/components/Content/Content" 6 | 7 | const Category = () => { 8 | const { id: categoryId } = useParams() 9 | 10 | const getEntries = partial(getCategoryEntries, categoryId) 11 | 12 | return ( 13 | markCategoryAsRead(categoryId)} 17 | /> 18 | ) 19 | } 20 | 21 | export default Category 22 | -------------------------------------------------------------------------------- /src/components/Article/ImageOverlayButton.css: -------------------------------------------------------------------------------- 1 | .icon-image { 2 | display: inline-block; 3 | width: auto; 4 | margin: 0; 5 | } 6 | 7 | .image-overlay-button { 8 | position: absolute; 9 | top: 0; 10 | left: 0; 11 | right: 0; 12 | bottom: 0; 13 | background-color: transparent; 14 | display: flex; 15 | justify-content: center; 16 | align-items: center; 17 | cursor: zoom-in; 18 | border: none; 19 | z-index: 1; 20 | } 21 | 22 | .image-wrapper { 23 | text-align: center; 24 | position: relative; 25 | } 26 | 27 | .image-container { 28 | display: inline-block; 29 | position: relative; 30 | width: 100%; 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/nanostores.js: -------------------------------------------------------------------------------- 1 | const createSetter = 2 | (store, key = null) => 3 | (updater) => { 4 | const state = store.get() 5 | 6 | if (typeof state === "object" && state !== null) { 7 | if (key === null) { 8 | return 9 | } 10 | if (typeof updater === "function") { 11 | store.set({ ...state, [key]: updater(state[key]) }) 12 | } else { 13 | store.set({ ...state, [key]: updater }) 14 | } 15 | } else if (typeof updater === "function") { 16 | store.set(updater(state)) 17 | } else { 18 | store.set(updater) 19 | } 20 | } 21 | 22 | export default createSetter 23 | -------------------------------------------------------------------------------- /src/components/Settings/FeedList.css: -------------------------------------------------------------------------------- 1 | .edit-modal { 2 | width: 400px; 3 | } 4 | 5 | .feed-table { 6 | width: 100%; 7 | } 8 | 9 | .feed-table .arco-checkbox-mask { 10 | border-radius: 0 !important; 11 | } 12 | 13 | .feed-table-action-bar { 14 | display: flex; 15 | align-items: center; 16 | width: 100%; 17 | } 18 | 19 | .feed-table-action-bar .button-group { 20 | display: flex; 21 | gap: 8px; 22 | padding-bottom: 16px; 23 | padding-left: 8px; 24 | } 25 | 26 | .search-input { 27 | margin-bottom: 16px; 28 | width: 300px; 29 | } 30 | 31 | @media screen and (max-width: 768px) { 32 | .edit-modal { 33 | width: 95%; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/Article/ImageLinkTag.jsx: -------------------------------------------------------------------------------- 1 | import { Tag, Tooltip } from "@arco-design/web-react" 2 | import { IconLink } from "@arco-design/web-react/icon" 3 | 4 | import "./ImageLinkTag.css" 5 | 6 | const ImageLinkTag = ({ href }) => { 7 | if (href === "#") { 8 | return null 9 | } 10 | 11 | return ( 12 | 13 | } 16 | onClick={(e) => { 17 | e.stopPropagation() 18 | window.open(href, "_blank") 19 | }} 20 | > 21 | {href} 22 | 23 | 24 | ) 25 | } 26 | 27 | export default ImageLinkTag 28 | -------------------------------------------------------------------------------- /src/components/Article/ArticleTOC.css: -------------------------------------------------------------------------------- 1 | .toc-droplist-container { 2 | background-color: var(--color-neutral-1); 3 | border: 1px solid var(--color-fill-3); 4 | border-radius: var(--border-radius-medium); 5 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); 6 | overflow: hidden; 7 | } 8 | 9 | .toc-filter-container { 10 | padding: 16px 16px 0; 11 | } 12 | 13 | .toc-menu-item { 14 | display: flex; 15 | align-items: center; 16 | width: 100%; 17 | } 18 | 19 | .toc-menu-container { 20 | max-height: 280px; 21 | width: 260px; 22 | } 23 | 24 | .toc-menu-container .arco-menu, 25 | .toc-menu-container .arco-menu-item { 26 | background-color: var(--color-neutral-1); 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | pnpm-debug.log* 10 | lerna-debug.log* 11 | 12 | dist 13 | dist-ssr 14 | *.local 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | 27 | # dependencies 28 | /node_modules 29 | /.pnp 30 | .pnp.js 31 | 32 | # testing 33 | /coverage 34 | 35 | # production 36 | /build 37 | 38 | # development 39 | /dev-dist 40 | 41 | # misc 42 | stats.html 43 | src/version-info.json 44 | -------------------------------------------------------------------------------- /src/hooks/usePhotoSlider.js: -------------------------------------------------------------------------------- 1 | import { useStore } from "@nanostores/react" 2 | import { map } from "nanostores" 3 | 4 | import createSetter from "@/utils/nanostores" 5 | 6 | const state = map({ isPhotoSliderVisible: false, selectedIndex: 0 }) 7 | 8 | const setIsPhotoSliderVisible = createSetter(state, "isPhotoSliderVisible") 9 | const setSelectedIndex = createSetter(state, "selectedIndex") 10 | 11 | const usePhotoSlider = () => { 12 | const { isPhotoSliderVisible, selectedIndex } = useStore(state) 13 | 14 | return { 15 | isPhotoSliderVisible, 16 | setIsPhotoSliderVisible, 17 | selectedIndex, 18 | setSelectedIndex, 19 | } 20 | } 21 | 22 | export default usePhotoSlider 23 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ReactFlux", 3 | "short_name": "ReactFlux", 4 | "description": "A Simple but Powerful RSS Reader for Miniflux", 5 | "icons": [ 6 | { 7 | "src": "favicon.ico", 8 | "sizes": "64x64 32x32 24x24 16x16", 9 | "type": "image/x-icon" 10 | }, 11 | { 12 | "src": "logo192.png", 13 | "type": "image/png", 14 | "sizes": "192x192" 15 | }, 16 | { 17 | "src": "logo512.png", 18 | "type": "image/png", 19 | "sizes": "512x512" 20 | } 21 | ], 22 | "theme_color": "#1F2327", 23 | "background_color": "#ffffff", 24 | "display": "fullscreen", 25 | "fullscreen": "true", 26 | "start_url": "/" 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/translator.yml: -------------------------------------------------------------------------------- 1 | name: "translator" 2 | 3 | on: 4 | issues: 5 | types: [opened, edited] 6 | issue_comment: 7 | types: [created, edited] 8 | pull_request_target: 9 | types: [opened, edited] 10 | pull_request_review_comment: 11 | types: [created, edited] 12 | 13 | jobs: 14 | translate: 15 | permissions: 16 | issues: write 17 | discussions: write 18 | pull-requests: write 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: lizheming/github-translate-action@1.1.2 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | with: 25 | IS_MODIFY_TITLE: true 26 | APPEND_TRANSLATION: true 27 | -------------------------------------------------------------------------------- /src/components/Main/Main.css: -------------------------------------------------------------------------------- 1 | .main { 2 | display: flex; 3 | grid-area: main; 4 | transition: 5 | padding-left 0.1s linear, 6 | width 0.1s linear; 7 | margin: 0; 8 | border: none; 9 | overflow: hidden; 10 | } 11 | 12 | .settings-modal { 13 | top: 5%; 14 | width: 720px; 15 | overflow-y: auto; 16 | } 17 | 18 | @media screen and (max-width: 992px) { 19 | .main { 20 | margin: 0; 21 | border: none; 22 | padding-left: 0; 23 | width: 100%; 24 | } 25 | } 26 | 27 | @media screen and (max-width: 768px) { 28 | .main { 29 | padding-bottom: env(safe-area-inset-bottom) !important; 30 | } 31 | .settings-modal { 32 | top: 8%; 33 | width: 95%; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | day: friday 8 | time: "18:00" 9 | timezone: Asia/Shanghai 10 | ignore: 11 | - dependency-name: react 12 | versions: [">=19.0.0"] 13 | - dependency-name: react-dom 14 | versions: [">=19.0.0"] 15 | - dependency-name: "@types/react" 16 | versions: [">=19.0.0"] 17 | - dependency-name: "@types/react-dom" 18 | versions: [">=19.0.0"] 19 | 20 | - package-ecosystem: github-actions 21 | directory: / 22 | schedule: 23 | interval: weekly 24 | day: friday 25 | time: "18:00" 26 | timezone: Asia/Shanghai 27 | -------------------------------------------------------------------------------- /.github/workflows/close-stale-issues.yml: -------------------------------------------------------------------------------- 1 | name: Close stale issues 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | workflow_dispatch: 8 | 9 | permissions: 10 | issues: write 11 | 12 | jobs: 13 | close_stale_issues: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/stale@v10 17 | with: 18 | repo-token: ${{ secrets.GITHUB_TOKEN }} 19 | stale-issue-message: "This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days." 20 | close-issue-message: "This issue was closed because it has been stalled for 5 days with no activity." 21 | days-before-issue-stale: 30 22 | days-before-issue-close: 5 23 | -------------------------------------------------------------------------------- /.github/workflows/dockerhub-description.yml: -------------------------------------------------------------------------------- 1 | name: Update Docker Hub Description 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - README.md 8 | - .github/workflows/dockerhub-description.yml 9 | 10 | workflow_dispatch: 11 | 12 | jobs: 13 | dockerHubDescription: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v6 17 | 18 | - name: Docker Hub Description 19 | uses: peter-evans/dockerhub-description@v5 20 | with: 21 | username: ${{ secrets.DOCKERHUB_USERNAME }} 22 | password: ${{ secrets.DOCKERHUB_TOKEN }} 23 | repository: ${{ github.repository }} 24 | short-description: ${{ github.event.repository.description }} 25 | enable-url-completion: true 26 | -------------------------------------------------------------------------------- /src/utils/form.js: -------------------------------------------------------------------------------- 1 | export const validateAndFormatFormFields = (form) => { 2 | // 获取当前表单所有字段的值 3 | const allFields = form.getFieldsValue() 4 | let isFormValid = true 5 | 6 | // 遍历所有字段,去除非密码字段的前后空格,并检查是否所有必填字段都已填写 7 | for (const [key, value] of Object.entries(allFields)) { 8 | if (key !== "password") { 9 | const trimmedValue = value?.trim() 10 | form.setFieldValue(key, trimmedValue) // 更新去除空格后的值 11 | if (!trimmedValue) { 12 | isFormValid = false // 如果去除空格后的必填字段为空,则表单不有效 13 | } 14 | } else if (!value) { 15 | isFormValid = false // 如果密码字段为空,则表单不有效 16 | } 17 | } 18 | 19 | return isFormValid 20 | } 21 | 22 | export const handleEnterKeyToSubmit = (event, form) => { 23 | if (event.key === "Enter") { 24 | form.submit() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/pages/Login.css: -------------------------------------------------------------------------------- 1 | .background { 2 | background-image: url("./images/background.jpg"); 3 | background-position: center; 4 | background-size: cover; 5 | background-repeat: no-repeat; 6 | width: 50%; 7 | height: 100%; 8 | } 9 | 10 | .form-panel { 11 | display: flex; 12 | flex-direction: column; 13 | justify-content: center; 14 | align-items: center; 15 | z-index: 2; 16 | background-color: var(--color-bg-1); 17 | width: 50%; 18 | height: 100%; 19 | overflow-y: auto; 20 | } 21 | 22 | .login-form { 23 | width: 340px; 24 | } 25 | 26 | .page-layout { 27 | display: flex; 28 | height: 100%; 29 | } 30 | 31 | @media screen and (max-width: 768px) { 32 | .form-panel { 33 | position: absolute !important; 34 | z-index: 2 !important; 35 | width: 100% !important; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/hooks/useScreenWidth.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react" 2 | 3 | const MEDIUM_THRESHOLD = 768 4 | const LARGE_THRESHOLD = 992 5 | 6 | const useScreenWidth = () => { 7 | const [isBelowMedium, setIsBelowMedium] = useState(window.innerWidth <= MEDIUM_THRESHOLD) 8 | const [isBelowLarge, setIsBelowLarge] = useState(window.innerWidth <= LARGE_THRESHOLD) 9 | 10 | useEffect(() => { 11 | const handleResize = () => { 12 | setIsBelowMedium(window.innerWidth <= MEDIUM_THRESHOLD) 13 | setIsBelowLarge(window.innerWidth <= LARGE_THRESHOLD) 14 | } 15 | 16 | window.addEventListener("resize", handleResize) 17 | return () => window.removeEventListener("resize", handleResize) 18 | }, []) 19 | 20 | return { isBelowMedium, isBelowLarge } 21 | } 22 | 23 | export default useScreenWidth 24 | -------------------------------------------------------------------------------- /src/components/Article/LoadingCards.jsx: -------------------------------------------------------------------------------- 1 | import { Card, Skeleton } from "@arco-design/web-react" 2 | import { useStore } from "@nanostores/react" 3 | 4 | import { contentState } from "@/store/contentState" 5 | import "./LoadingCards.css" 6 | 7 | const LoadingCard = ({ isArticleListReady }) => ( 8 | 9 | 12 | } 13 | /> 14 | 15 | ) 16 | 17 | const LoadingCards = () => { 18 | const { isArticleListReady } = useStore(contentState) 19 | 20 | return ( 21 | !isArticleListReady && 22 | Array.from({ length: 4 }, (_, index) => ( 23 | 24 | )) 25 | ) 26 | } 27 | 28 | export default LoadingCards 29 | -------------------------------------------------------------------------------- /src/pages/ErrorPage.jsx: -------------------------------------------------------------------------------- 1 | import { Button, Result } from "@arco-design/web-react" 2 | import { useEffect } from "react" 3 | import { useNavigate, useRouteError } from "react-router" 4 | 5 | import hideSpinner from "@/utils/loading" 6 | 7 | const ErrorPage = () => { 8 | const navigate = useNavigate() 9 | const error = useRouteError() 10 | console.error(error) 11 | 12 | useEffect(() => { 13 | hideSpinner() 14 | }, []) 15 | 16 | return ( 17 |
18 | navigate("/")}> 23 | Back to Home 24 | , 25 | ]} 26 | /> 27 |
28 | ) 29 | } 30 | 31 | export default ErrorPage 32 | -------------------------------------------------------------------------------- /src/components/Sidebar/AddFeed.jsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@arco-design/web-react" 2 | import { IconPlus } from "@arco-design/web-react/icon" 3 | import { useStore } from "@nanostores/react" 4 | 5 | import CustomTooltip from "@/components/ui/CustomTooltip" 6 | import { polyglotState } from "@/hooks/useLanguage" 7 | import useModalToggle from "@/hooks/useModalToggle" 8 | 9 | export default function AddFeed() { 10 | const { setAddFeedModalVisible } = useModalToggle() 11 | const { polyglot } = useStore(polyglotState) 12 | 13 | return ( 14 | 15 |