├── .editorconfig ├── .env ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── .gitmodules ├── .kktrc.js ├── README.md ├── chrome-main ├── manifest.json └── osc-logo.png ├── img ├── newtab.gif ├── newtab1.gif ├── newtab2.gif ├── osc-extensions.png ├── osc-news1.png ├── osc-news2.png ├── osc-news3.png ├── osc-news4.png ├── osc-news5.png ├── osc-news6.png ├── osc-news7.png ├── osc-news8.png └── oschina.svg ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html ├── renovate.json └── src ├── Root.js ├── Route.js ├── assets ├── add-icon.png ├── apple.svg ├── chrome-app.svg ├── gitee.svg ├── github.svg ├── linux-logo.svg ├── oschina.svg ├── setting.svg └── website.svg ├── component ├── Clock │ ├── index.js │ └── index.module.less ├── Contextmenu │ ├── index.js │ └── index.module.less ├── Dropdown │ ├── index.js │ └── index.module.less ├── Footer.js ├── Footer.module.less ├── Header.js ├── Header.module.less ├── Icon │ └── index.js ├── Loading │ ├── index.js │ └── index.module.less ├── Modal │ └── index.module.less ├── OSCNews.js ├── OSCNews.module.less ├── Progress │ ├── index.js │ └── index.module.less ├── Search │ ├── index.js │ └── index.module.less ├── Select │ ├── index.js │ └── index.module.less ├── Switch │ ├── index.js │ └── index.module.less ├── container │ ├── index.js │ └── index.module.less └── modal │ └── index.js ├── index.js ├── index.less ├── pages ├── Blank.js ├── Blank.module.less ├── Document │ ├── icons.js │ ├── index.js │ └── index.module.less ├── Github.js ├── Github.module.less ├── History.js ├── History.module.less ├── Linux │ ├── index.js │ └── index.module.less ├── Navigation │ ├── Edit.js │ ├── Edit.module.less │ ├── index.js │ └── index.module.less ├── Search │ ├── index.js │ └── index.module.less └── Todo │ ├── icon.js │ ├── index.js │ └── index.module.less ├── source ├── search.json ├── trending.json └── website.json └── utils ├── BlockFetch.js ├── fetch.js ├── index.js └── theWeek.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = false 9 | insert_final_newline = false -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | INLINE_RUNTIME_CHUNK=false 2 | FAST_REFRESH=false 3 | BUILD_PATH=dist -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | # github: [jaywcjlove] 4 | # patreon: # Replace with a single Patreon username 5 | # open_collective: # Replace with a single Open Collective username 6 | # ko_fi: # Replace with a single Ko-fi username 7 | # tidelift: #npm/mocker-api 8 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | custom: https://jaywcjlove.github.io/#/sponsor 10 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@master 12 | with: 13 | submodules: true 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version: 18 17 | registry-url: 'https://registry.npmjs.org' 18 | 19 | - run: npm install --unsafe-perm 20 | - run: npm run build 21 | - run: mkdir -p build 22 | - run: cp -rp img/* build 23 | 24 | - name: Converts Markdown to HTML 25 | uses: jaywcjlove/markdown-to-html-cli@main 26 | with: 27 | output: build/index.html 28 | github-corners: https://github.com/jaywcjlove/oscnews 29 | favicon: data:image/svg+xml,🌐 30 | 31 | - name: Generate Contributors Images 32 | uses: jaywcjlove/github-action-contributors@main 33 | with: 34 | filter-author: (renovate\[bot\]|renovate-bot|dependabot\[bot\]) 35 | output: build/CONTRIBUTORS.svg 36 | avatarSize: 42 37 | 38 | - name: Create Tag 39 | id: create_tag 40 | uses: jaywcjlove/create-tag-action@main 41 | with: 42 | package-path: ./package.json 43 | 44 | - name: get tag version 45 | id: tag_version 46 | uses: jaywcjlove/changelog-generator@main 47 | 48 | - name: Deploy 49 | uses: peaceiris/actions-gh-pages@v3 50 | with: 51 | commit_message: ${{steps.tag_version.outputs.tag}} ${{ github.event.head_commit.message }} 52 | github_token: ${{ secrets.GITHUB_TOKEN }} 53 | publish_dir: ./build 54 | 55 | - name: Look Changelog 56 | id: changelog 57 | uses: jaywcjlove/changelog-generator@main 58 | with: 59 | token: ${{ secrets.GITHUB_TOKEN }} 60 | filter-author: (jaywcjlove|小弟调调™|dependabot\[bot\]|Renovate Bot) 61 | filter: '[R|r]elease[d]\s+[v|V]\d(\.\d+){0,2}' 62 | 63 | - name: Create Release 64 | uses: ncipollo/release-action@v1 65 | if: steps.create_tag.outputs.successful 66 | with: 67 | token: ${{ secrets.GITHUB_TOKEN }} 68 | name: ${{ steps.create_tag.outputs.version }} 69 | tag: ${{ steps.create_tag.outputs.version }} 70 | body: | 71 | [![Buy me a coffee](https://img.shields.io/badge/Buy%20me%20a%20coffee-048754?logo=buymeacoffee)](https://jaywcjlove.github.io/#/sponsor) [Chrome 网上商店](https://chrome.google.com/webstore/detail/oscnews/iheapfheanfjcemgneblljhaebonakbg) 搜索 `oscnews` 安装,或者直接下载 [crx 文件](https://github.com/jaywcjlove/oscnews/releases) 安装,打开 [chrome://extensions](chrome://extensions/) 将 crx 拖拽到扩展列表中安装。 72 | 73 | [![](http://jaywcjlove.github.io/sb/download/chrome-web-store.svg)](https://chrome.google.com/webstore/detail/oscnews/iheapfheanfjcemgneblljhaebonakbg) 74 | 75 | Documentation ${{ steps.changelog.outputs.tag }}: https://raw.githack.com/jaywcjlove/oscnews/${{ steps.changelog.outputs.gh-pages-short-hash }}/index.html 76 | Comparing Changes: ${{ steps.changelog.outputs.compareurl }} 77 | 78 | ${{ steps.changelog.outputs.changelog }} 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | oscnews*/ 3 | build* 4 | dist* 5 | 6 | .DS_Store 7 | .cache 8 | .rdoc-dist 9 | .vscode 10 | 11 | *.bak 12 | *.tem 13 | *.temp 14 | #.swp 15 | *.*~ 16 | ~*.* 17 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/dev-site"] 2 | path = src/dev-site 3 | url = https://github.com/jaywcjlove/dev-site.git 4 | -------------------------------------------------------------------------------- /.kktrc.js: -------------------------------------------------------------------------------- 1 | import { CleanWebpackPlugin } from 'clean-webpack-plugin'; 2 | import FileManagerPlugin from 'filemanager-webpack-plugin'; 3 | import lessModules from '@kkt/less-modules'; 4 | 5 | export default (conf, env, options) => { 6 | conf = lessModules(conf, env, options); 7 | conf.output.publicPath = './'; 8 | 9 | const regexp = /(ReactRefreshWebpackPlugin)/; 10 | conf.plugins = conf.plugins.map((item) => { 11 | if (item.constructor && item.constructor.name && regexp.test(item.constructor.name)) { 12 | return null; 13 | } 14 | return item; 15 | }).filter(Boolean); 16 | 17 | conf.plugins.push(new CleanWebpackPlugin({ 18 | // cleanStaleWebpackAssets: true 19 | })); 20 | 21 | conf.plugins.push( 22 | new FileManagerPlugin({ 23 | events: { 24 | onEnd: { 25 | copy: [ 26 | { source: './chrome-main/manifest.json', destination: './dist/manifest.json' }, 27 | { source: './chrome-main/background.js', destination: './dist/background.js' }, 28 | { source: './chrome-main/osc-logo.png', destination: './dist/osc-logo.png' }, 29 | { source: './src/dev-site/public/icons', destination: './dist/icons' }, 30 | ], 31 | }, 32 | }, 33 | }), 34 | ); 35 | return conf; 36 | } 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 | 7 | [![Buy me a coffee](https://img.shields.io/badge/Buy%20me%20a%20coffee-048754?logo=buymeacoffee)](https://jaywcjlove.github.io/#/sponsor) 8 | 9 | 这是一个Chrome插件,在新标签页查看开源中国[软件更新资讯](http://www.oschina.net/news/project),搜索页面、GitHub 趋势榜、linux 命令索引,技术文档导航([web版](http://jaywcjlove.github.io/dev-site/)),浏览历史记录,时钟页面。 10 | 11 |

12 | 13 |

14 | 15 | ## 商店安装 16 | 17 | [Chrome 网上商店](https://chrome.google.com/webstore/detail/oscnews/iheapfheanfjcemgneblljhaebonakbg) 搜索 `oscnews` 安装,或者直接下载 [crx 文件](https://github.com/jaywcjlove/oscnews/releases) 安装,打开 [chrome://extensions](chrome://extensions/) 将 crx 拖拽到扩展列表中安装。 18 | 19 | [![](http://jaywcjlove.github.io/sb/download/chrome-web-store.svg)](https://chrome.google.com/webstore/detail/oscnews/iheapfheanfjcemgneblljhaebonakbg) 20 | 21 | 22 | > [开源中国下载 oscnews.crx 文件](https://gitee.com/jaywcjlove/oscnews/releases) 23 | > [Github下载 oscnews.crx 文件](https://github.com/jaywcjlove/oscnews/releases) 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 | 36 | #### 网址导航 37 | 38 | - 删除网址:摁住 `alt` 键,出现删除按钮,也可以右键 39 | 40 | 41 | 42 | #### 任务清单 43 | 44 | 45 | 46 | #### 兼容FireFox 47 | 48 | 只需要安装 [Chrome Store Foxified ](https://addons.mozilla.org/zh-CN/firefox/addon/chrome-store-foxified/) 扩展,就能直接在 Firefox 里安装 Chrome 扩展,不过,还是需要先登录 https://addons.mozilla.org/ 然后就可以打开 [Chrome 网上商店](https://chrome.google.com/webstore/detail/oscnews/iheapfheanfjcemgneblljhaebonakbg) 里的 oscnews 。点击 ADD TO FIREFOX 后,Chrome Store Foxified 就在后台工作了,最终会在 addons 里提交一个临时扩展,并且安装在你的 Firefox 里。 49 | 50 | > [Github下载 oscnews.xpi 文件](https://github.com/jaywcjlove/oscnews/releases) 51 | 52 | 下载 xpi 文件,将 xpi 文件拖拽到扩展列表中安装。 53 | 54 | ## 开发模式安装 55 | 56 | 下载源文件 57 | 58 | ```bash 59 | git clone https://github.com/jaywcjlove/oscnews.git --depth=1 --recurse-submodules 60 | # 开源中国仓库 61 | git clone https://gitee.com/jaywcjlove/oscnews.git --depth=1 --recurse-submodules 62 | ``` 63 | 64 | 安装依赖 65 | 66 | ```bash 67 | npm install 68 | ``` 69 | 70 | 编译源码 71 | 72 | ```bash 73 | npm run build 74 | ``` 75 | 76 | 下载编译之后,在 Chrome 浏览器地址栏输入 [chrome://extensions](chrome://extensions/) 打开插件界面,通过下图方式,将生成的 `oscnews` 目录,导入到插件列表中。 77 | 78 | ![](./img/osc-extensions.png) 79 | 80 | 81 | 应用商店生成 crx 文件 82 | 83 | ```diff 84 | - https://clients2.google.com/service/update2/crx?response=redirect&x=id%3D<这里是扩展ID>%26uc&prodversion=32 85 | + https://clients2.google.com/service/update2/crx?response=redirect&x=id%3Diheapfheanfjcemgneblljhaebonakbg%26uc&prodversion=32 86 | ``` 87 | 88 | Mac系统下扩展的源码目录 89 | 90 | ```bash 91 | cd ~/Library/Application\ Support/Google/Chrome/Default/Extensions 92 | ``` 93 | 94 | ## TODO 95 | 96 | - [x] 浏览历史记录 97 | - [x] 浏览历史选择今天、周、全部 98 | - [x] 清空历史记录 99 | - [x] 开发文档导航 100 | - [ ] 自定义开发文档导航 101 | - [x] 开发文档导航搜索过滤 102 | - [x] ~~添加删除文档?~~ 103 | - [x] 文档分类前端(前端、后端、工具) 104 | - [x] Linux命令检索,集成 [linux-command](https://github.com/jaywcjlove/linux-command) 105 | - [x] Github 趋势榜天、周、月统计切换,语言切换 106 | - [x] 配置存储使用 [chrome.storage](https://developer.chrome.com/apps/storage) 替代 107 | - [x] 添加搜索引擎页面 108 | - [x] 开源中国新闻 109 | - [x] 宽度拖拽设置 110 | - [ ] 下拉翻页 111 | - [x] 空页面 112 | - [ ] 天气日期展示 113 | - [x] 农历日期显示 114 | - [x] 更换背景色 115 | - [x] 添加时钟效果 116 | - [x] 常用网站导航 117 | - [x] 自定义常用网站导航 118 | - [ ] 书签管理页面 119 | - [ ] Github 120 | - [ ] Github 登录,浏览自己项目 121 | - [ ] Github Start 管理 122 | - [ ] Gitlab 登录,相关功能 123 | - [ ] 集成 [octotree](https://github.com/buunguyen/octotree) 部分功能 124 | - [ ] 插件官方网站 125 | - [ ] 密码管理器 126 | - [ ] RSS订阅功能 127 | - [x] 提醒事项 128 | - [x] 记录代办事项 129 | - [ ] Chrome通知 130 | - [x] 设置功能 131 | - [x] 设置是否替换新标签页显示 132 | - [ ] 设置URL是否在新的标签页打开, 133 | - [ ] 菜单配置 134 | - [ ] 清空历史 135 | - [ ] 分享应用到微博 136 | - [ ] 兼容其它浏览器 137 | - [x] 兼容QQ浏览器,已测试下载crx文件安装直接可以用 138 | - [ ] 兼容360急速浏览器 139 | - [ ] 兼容Firefox浏览器 140 | 141 | ## License 142 | 143 | The MIT License (MIT) -------------------------------------------------------------------------------- /chrome-main/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oscnews", 3 | "version": "1.16.0", 4 | "description": "查看开源中国软件更新资讯,内置文档导航,GitHub 趋势榜,linux命令索引,浏览历史记录和时钟页面。", 5 | "icons": { 6 | "128": "osc-logo.png" 7 | }, 8 | "permissions": [ 9 | "tabs", 10 | "activeTab", 11 | "unlimitedStorage", 12 | "storage", 13 | "history", 14 | "management", 15 | "webRequest", 16 | "scripting" 17 | ], 18 | "host_permissions": ["https://www.oschina.net/", "https://github.com/"], 19 | "manifest_version": 3, 20 | "offline_enabled": true, 21 | "chrome_url_overrides": { 22 | "newtab": "index.html" 23 | } 24 | } -------------------------------------------------------------------------------- /chrome-main/osc-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaywcjlove/oscnews/9cd92dc9ba98247628007b57350b3d8cb71efb07/chrome-main/osc-logo.png -------------------------------------------------------------------------------- /img/newtab.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaywcjlove/oscnews/9cd92dc9ba98247628007b57350b3d8cb71efb07/img/newtab.gif -------------------------------------------------------------------------------- /img/newtab1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaywcjlove/oscnews/9cd92dc9ba98247628007b57350b3d8cb71efb07/img/newtab1.gif -------------------------------------------------------------------------------- /img/newtab2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaywcjlove/oscnews/9cd92dc9ba98247628007b57350b3d8cb71efb07/img/newtab2.gif -------------------------------------------------------------------------------- /img/osc-extensions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaywcjlove/oscnews/9cd92dc9ba98247628007b57350b3d8cb71efb07/img/osc-extensions.png -------------------------------------------------------------------------------- /img/osc-news1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaywcjlove/oscnews/9cd92dc9ba98247628007b57350b3d8cb71efb07/img/osc-news1.png -------------------------------------------------------------------------------- /img/osc-news2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaywcjlove/oscnews/9cd92dc9ba98247628007b57350b3d8cb71efb07/img/osc-news2.png -------------------------------------------------------------------------------- /img/osc-news3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaywcjlove/oscnews/9cd92dc9ba98247628007b57350b3d8cb71efb07/img/osc-news3.png -------------------------------------------------------------------------------- /img/osc-news4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaywcjlove/oscnews/9cd92dc9ba98247628007b57350b3d8cb71efb07/img/osc-news4.png -------------------------------------------------------------------------------- /img/osc-news5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaywcjlove/oscnews/9cd92dc9ba98247628007b57350b3d8cb71efb07/img/osc-news5.png -------------------------------------------------------------------------------- /img/osc-news6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaywcjlove/oscnews/9cd92dc9ba98247628007b57350b3d8cb71efb07/img/osc-news6.png -------------------------------------------------------------------------------- /img/osc-news7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaywcjlove/oscnews/9cd92dc9ba98247628007b57350b3d8cb71efb07/img/osc-news7.png -------------------------------------------------------------------------------- /img/osc-news8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaywcjlove/oscnews/9cd92dc9ba98247628007b57350b3d8cb71efb07/img/osc-news8.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oscnews", 3 | "version": "1.16.0", 4 | "description": "开源中国新闻,查看开源中国软件更新资讯,文档导航,GitHub 趋势榜,和时钟页面。", 5 | "private": true, 6 | "main": "index.js", 7 | "scripts": { 8 | "start": "BUILD_PATH=dist kkt start --watch --no-open-browser", 9 | "build": "BUILD_PATH=dist kkt build" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/jaywcjlove/oscnews.git" 14 | }, 15 | "keywords": [], 16 | "author": "", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/jaywcjlove/oscnews/issues" 20 | }, 21 | "homepage": "https://github.com/jaywcjlove/oscnews#readme", 22 | "devDependencies": { 23 | "@kkt/less-modules": "~7.1.1", 24 | "clean-webpack-plugin": "~4.0.0", 25 | "filemanager-webpack-plugin": "~6.1.7", 26 | "kkt": "~7.1.5" 27 | }, 28 | "dependencies": { 29 | "@wcj/dark-mode": "~1.0.13", 30 | "cheerio": "~1.0.0-rc.10", 31 | "classnames": "~2.3.1", 32 | "linux-command": "~1.9.0", 33 | "prop-types": "~15.8.1", 34 | "react": "~17.0.2", 35 | "react-dom": "~17.0.2", 36 | "solarlunar": "~2.0.7" 37 | }, 38 | "eslintConfig": { 39 | "extends": "react-app" 40 | }, 41 | "browserslist": { 42 | "production": [ 43 | ">0.2%", 44 | "not dead", 45 | "not op_mini all" 46 | ], 47 | "development": [ 48 | "last 1 chrome version", 49 | "last 1 firefox version", 50 | "last 1 safari version" 51 | ] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaywcjlove/oscnews/9cd92dc9ba98247628007b57350b3d8cb71efb07/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | OSC 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "packageRules": [ 6 | { 7 | "matchPackagePatterns": ["*"], 8 | "rangeStrategy": "replace" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/Root.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Container from './component/container'; 3 | import { getNavData } from './Route'; 4 | 5 | export default class Root extends Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | config: props.config, 10 | }; 11 | } 12 | componentDidMount() { 13 | const { config } = this.state; 14 | // eslint-disable-next-line 15 | chrome.storage.onChanged.addListener((changes) => { 16 | for (const i in changes) { 17 | if (Object.prototype.hasOwnProperty.call(changes, i)) { 18 | config[i] = changes[i].newValue; 19 | } 20 | } 21 | this.setState({ config }); 22 | }); 23 | } 24 | render() { 25 | const { config } = this.state; 26 | config.menus = getNavData().filter((item) => { 27 | return { title: item.title, type: item.type }; 28 | }); 29 | return ( 30 | 31 | {getNavData().map((item, idx) => { 32 | if (!item.component) return null; 33 | const Comp = item.component.default || item.component; 34 | return Comp ? : null; 35 | })} 36 | 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Route.js: -------------------------------------------------------------------------------- 1 | 2 | import Blank from './pages/Blank'; 3 | // import Github from './pages/Github'; 4 | import Historys from './pages/History'; 5 | import Documents from './pages/Document'; 6 | import Navigation from './pages/Navigation'; 7 | import Linux from './pages/Linux'; 8 | import Search from './pages/Search'; 9 | import Todo from './pages/Todo'; 10 | 11 | export const getNavData = () => [ 12 | { 13 | title: '空白页', 14 | type: 'blank', 15 | component: Blank, 16 | }, { 17 | title: '导航', 18 | type: 'navigation', 19 | component: Navigation, 20 | }, { 21 | title: '清单', 22 | type: 'todo', 23 | component: Todo, 24 | }, { 25 | title: '搜索', 26 | type: 'search', 27 | component: Search, 28 | }, { 29 | title: '命令', 30 | type: 'linux', 31 | component: Linux, 32 | }, { 33 | title: '开发文档', 34 | type: 'document', 35 | component: Documents, 36 | }, 37 | // { 38 | // title: '趋势榜', 39 | // type: 'trending', 40 | // component: Github, 41 | // }, 42 | { 43 | title: '历史记录', 44 | type: 'history', 45 | component: Historys, 46 | }, 47 | ]; 48 | -------------------------------------------------------------------------------- /src/assets/add-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaywcjlove/oscnews/9cd92dc9ba98247628007b57350b3d8cb71efb07/src/assets/add-icon.png -------------------------------------------------------------------------------- /src/assets/apple.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/chrome-app.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/gitee.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/linux-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/setting.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/website.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/component/Clock/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import classNames from 'classnames'; 3 | import styles from './index.module.less'; 4 | 5 | 6 | const rotations = [0, 0, 0]; // [seconds, minutes, hours] 7 | 8 | export default class Clock extends Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = {}; 12 | } 13 | setTime() { 14 | const now = new Date(); 15 | const seconds = now.getSeconds(); 16 | const minutes = now.getMinutes(); 17 | const hours = now.getHours() % 12; 18 | 19 | if (seconds === 0) { 20 | rotations[0] += 1; 21 | } 22 | 23 | if (minutes === 0 && seconds === 0) { 24 | rotations[1] += 1; 25 | } 26 | 27 | if (hours === 0 && minutes === 0 && seconds === 0) { 28 | rotations[2] += 1; 29 | } 30 | 31 | const secondsDeg = ((seconds / 60) * 360) + (rotations[0] * 360); 32 | const minutesDeg = ((minutes / 60) * 360) + (rotations[1] * 360); 33 | const hoursDeg = ((hours / 12) * 360) + ((minutes / 60) * 30) + (rotations[2] * 360); 34 | if (this.handHour) { 35 | this.handHour.style.transform = `rotate(${hoursDeg}deg)`; 36 | } 37 | if (this.handMinute) { 38 | this.handMinute.style.transform = `rotate(${minutesDeg}deg)`; 39 | } 40 | if (this.handSecond) { 41 | this.handSecond.style.transform = `rotate(${secondsDeg}deg)`; 42 | } 43 | } 44 | componentWillUnmount() { 45 | if (this.interval) clearInterval(this.interval); 46 | } 47 | componentDidMount() { 48 | this.setTime(); 49 | this.interval = setInterval(() => { 50 | this.setTime(); 51 | }, 1000); 52 | } 53 | render() { 54 | return ( 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | this.handHour = node} className={classNames(styles.hand, styles.handHour)} x1="50" y1="25" x2="50" y2="50" /> 71 | this.handMinute = node} className={classNames(styles.hand, styles.handMinute)} x1="50" y1="10" x2="50" y2="50" /> 72 | 73 | this.handSecond = node} className={classNames(styles.hand, styles.handSecond)}> 74 | 75 | 76 | 77 | 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/component/Clock/index.module.less: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-primary: #212121; 3 | --color-accent: #F44336; 4 | --color-background: #FFF; 5 | } 6 | 7 | @keyframes fade-in { 8 | from { opacity: 0; } 9 | to { opacity: 1; } 10 | } 11 | 12 | * { 13 | transform-origin: inherit; 14 | } 15 | 16 | 17 | .clock { 18 | width: 140px; 19 | height: 140px; 20 | color: var(--color-theme-text); 21 | fill: currentColor; 22 | transform-origin: 50px 50px; 23 | animation-name: fade-in; 24 | animation-duration: 500ms; 25 | animation-fill-mode: both; 26 | } 27 | .clock line { 28 | stroke: currentColor; 29 | stroke-linecap: round; 30 | } 31 | .marks { 32 | opacity: 0.7; 33 | stroke-width: 1px; 34 | line { 35 | stroke: currentColor; 36 | stroke-linecap: round; 37 | } 38 | } 39 | .mark1 { transform: rotate(30deg); } 40 | .mark2 { transform: rotate(60deg); } 41 | .mark3 { transform: rotate(90deg); } 42 | .mark4 { transform: rotate(120deg); } 43 | .mark5 { transform: rotate(150deg); } 44 | .mark6 { transform: rotate(180deg); } 45 | .mark7 { transform: rotate(210deg); } 46 | .mark8 { transform: rotate(240deg); } 47 | .mark9 { transform: rotate(270deg); } 48 | .mark10 { transform: rotate(300deg); } 49 | .mark11 { transform: rotate(330deg); } 50 | .mark12 { transform: rotate(360deg); } 51 | 52 | .hand { 53 | stroke-width: 1.5px; 54 | transition: transform 200ms cubic-bezier(0.175, 0.885, 0.32, 1.275); 55 | } 56 | 57 | .handSecond { 58 | color: var(--color-accent); 59 | stroke-width: 1px; 60 | } -------------------------------------------------------------------------------- /src/component/Contextmenu/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import PropTypes from 'prop-types'; 4 | import styles from './index.module.less'; 5 | 6 | export default class Contextmenu extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | visible: props.visible, 11 | }; 12 | this.handleContextmenu = this.handleContextmenu.bind(this); 13 | this.handleClickOutside = this.handleClickOutside.bind(this); 14 | } 15 | componentDidMount() { 16 | document.addEventListener('contextmenu', this.handleContextmenu, false); 17 | document.addEventListener('mousedown', this.handleClickOutside, true); 18 | } 19 | componentWillUnmount() { 20 | document.removeEventListener('contextmenu', this.handleContextmenu, false); 21 | document.removeEventListener('mousedown', this.handleClickOutside, true); 22 | } 23 | componentDidUpdate(prevProps, prevState) { 24 | if (prevProps.visible !== this.props.visible) { 25 | this.setState({ visible: this.props.visible }); 26 | } 27 | } 28 | handleClickOutside(e) { 29 | const { onClickOutside } = this.props; 30 | // Ignore clicks on the component it self 31 | // https://codepen.io/graubnla/pen/EgdgZm 32 | // Detect a click outside of a React Component 33 | // https://www.dhariri.com/posts/57c724e4d1befa66e5b8e056 34 | const domNode = ReactDOM.findDOMNode(this); 35 | if (!domNode || !domNode.contains(e.target) || domNode === e.target) { 36 | this.setState({ visible: false }, () => { 37 | onClickOutside(); 38 | }); 39 | } 40 | } 41 | handleContextmenu(e) { 42 | e.preventDefault(); 43 | this.setState({ visible: true }, () => { 44 | if (!e || !this.menu) return; 45 | // 根据事件对象中鼠标点击的位置,进行定位 46 | const winHeight = document.documentElement.clientHeight; 47 | const winWidth = document.documentElement.clientWidth; 48 | if (winHeight - e.clientY > this.menu.clientHeight) { 49 | this.menu.style.top = `${e.clientY}px`; 50 | } else { 51 | this.menu.style.bottom = 0; 52 | } 53 | if (winWidth - e.clientX > this.menu.clientWidth) { 54 | this.menu.style.left = `${e.clientX}px`; 55 | } else { 56 | this.menu.style.bottom = 0; 57 | } 58 | }); 59 | } 60 | onClickMenuItem(item) { 61 | const { onClick } = this.props; 62 | this.setState({ visible: false }); 63 | onClick(item); 64 | } 65 | render() { 66 | const { visible } = this.state; 67 | const { option } = this.props; 68 | if (!visible || option.length < 1) return null; 69 | return ( 70 |
this.menu = node}> 71 | {option.map((item, idx) => ( 72 |
{item.label}
73 | ))} 74 |
75 | ); 76 | } 77 | } 78 | 79 | Contextmenu.propsTypes = { 80 | option: PropTypes.array, 81 | onClick: PropTypes.func, 82 | onClickOutside: PropTypes.func, 83 | visible: PropTypes.bool, 84 | }; 85 | 86 | Contextmenu.defaultProps = { 87 | visible: false, 88 | option: [], 89 | onClick() {}, 90 | onClickOutside() {}, 91 | }; 92 | -------------------------------------------------------------------------------- /src/component/Contextmenu/index.module.less: -------------------------------------------------------------------------------- 1 | 2 | .menu { 3 | position: absolute; 4 | color: #586069; 5 | box-shadow: 0 3px 12px rgba(27, 31, 35, 0.15); 6 | background-color: #fff; 7 | background-clip: padding-box; 8 | border: 1px solid rgba(27, 31, 35, 0.15); 9 | border-radius: 4px; 10 | padding: 4px 0; 11 | z-index: 9999; 12 | .item { 13 | padding: 6px 8px; 14 | color: #868686; 15 | text-overflow: ellipsis; 16 | white-space: nowrap; 17 | overflow: hidden; 18 | cursor: pointer; 19 | transition: background-color 0.3s; 20 | &:hover { 21 | background-color: #eaeaea; 22 | color: #333; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/component/Dropdown/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import classNames from 'classnames'; 4 | import styles from './index.module.less'; 5 | 6 | export default class Dropdown extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | visible: false, 11 | }; 12 | this.handleClickOutside = this.handleClickOutside.bind(this); 13 | } 14 | onClick() { 15 | this.setState({ 16 | visible: !this.state.visible, 17 | }); 18 | } 19 | componentDidMount() { 20 | document.addEventListener('mousedown', this.handleClickOutside, true); 21 | } 22 | componentWillUnmount() { 23 | document.removeEventListener('mousedown', this.handleClickOutside, true); 24 | } 25 | handleClickOutside(e) { 26 | // Ignore clicks on the component it self 27 | // https://codepen.io/graubnla/pen/EgdgZm 28 | // Detect a click outside of a React Component 29 | // https://www.dhariri.com/posts/57c724e4d1befa66e5b8e056 30 | const domNode = ReactDOM.findDOMNode(this); 31 | if (!domNode || !domNode.contains(e.target) || domNode === e.target) { 32 | this.setState({ visible: false }); 33 | } 34 | } 35 | renderObjectMenu(menu) { 36 | return menu.map(({ title, line, panel, url, divider, target, ...otherProps }, idx) => { 37 | const titleView = title && typeof title === 'function' ? title() : title; 38 | const cls = classNames(styles.item, { panel }); 39 | if (line && !divider && !title) { 40 | return ( 41 |
42 | ); 43 | } else if (divider) { 44 | return ( 45 |
{titleView}
46 | ); 47 | } else if (url) { 48 | return {titleView}; 49 | } else if (titleView) { 50 | return
{titleView}
; 51 | } 52 | return null; 53 | }); 54 | } 55 | render() { 56 | const { className, children, menu } = this.props; 57 | const { visible } = this.state; 58 | const objectView = typeof menu !== 'object' ? menu : this.renderObjectMenu(menu); 59 | return ( 60 |
61 |
{children}
62 |
67 | {React.isValidElement(menu) ? menu : objectView} 68 |
69 |
70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/component/Dropdown/index.module.less: -------------------------------------------------------------------------------- 1 | .warpper { 2 | position: relative; 3 | } 4 | .btn { 5 | cursor: pointer; 6 | } 7 | .menu { 8 | position: absolute; 9 | user-select: none; 10 | min-width: 120px; 11 | min-height: 21px; 12 | right: 0; 13 | margin-top: 4px; 14 | font-size: 14px; 15 | background-color: var(--color-theme-bg); 16 | background-clip: padding-box; 17 | border: 1px solid var(--color-border-color); 18 | border-radius: 4px; 19 | box-shadow: 0 3px 12px rgba(27,31,35,0.15); 20 | transition: all .3s; 21 | z-index: 99; 22 | &::before, &::after { 23 | content: ''; 24 | display: block; 25 | height: 3px; 26 | } 27 | &:global { 28 | &.hide { 29 | display: none; 30 | } 31 | &.show { 32 | display: block; 33 | } 34 | } 35 | a { 36 | text-decoration: none; 37 | } 38 | .item { 39 | display: block !important; 40 | padding: 6px 8px !important; 41 | text-overflow: ellipsis; 42 | white-space: nowrap; 43 | overflow: hidden; 44 | transition: background-color .3s; 45 | &:hover { 46 | background-color: #eaeaea; 47 | color: #333; 48 | } 49 | &:active { 50 | background-color: #dedede; 51 | } 52 | &:global(.panel) { 53 | text-overflow: inherit; 54 | white-space: inherit; 55 | overflow: inherit; 56 | &:hover { 57 | background-color: transparent; 58 | color: #868686; 59 | } 60 | } 61 | img { 62 | display: inline-block; 63 | } 64 | } 65 | .divider { 66 | padding: 9px 10px 9px 10px; 67 | font-size: 12px; 68 | } 69 | :global(.line) { 70 | border-top: 1px solid var(--color-border-color); 71 | } 72 | } -------------------------------------------------------------------------------- /src/component/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './Footer.module.less'; 3 | 4 | export default function Footer({ children }) { 5 | return ( 6 |
{children}
7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /src/component/Footer.module.less: -------------------------------------------------------------------------------- 1 | 2 | .footer { 3 | text-align: center; 4 | padding: 20px 0; 5 | font-size: 14px; 6 | color: #d2d2d2; 7 | a { 8 | color: #d2d2d2; 9 | } 10 | span::after, span::before { 11 | content: ''; 12 | display: inline-block; 13 | width: 35px; 14 | height: 1px; 15 | background-color: #dcdcdc; 16 | top: -4px; 17 | position: relative; 18 | } 19 | span::after { 20 | margin-left: 5px; 21 | } 22 | span::before { 23 | margin-right: 5px; 24 | } 25 | } -------------------------------------------------------------------------------- /src/component/Header.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import '@wcj/dark-mode'; 4 | import classNames from 'classnames'; 5 | import Dropdown from './Dropdown'; 6 | import Switch from './Switch'; 7 | import logo from '../assets/oschina.svg'; 8 | import chromeApp from '../assets/chrome-app.svg'; 9 | // import gitee from '../assets/gitee.svg'; 10 | // import github from '../assets/github.svg'; 11 | // import apple from '../assets/apple.svg'; 12 | import styles from './Header.module.less'; 13 | 14 | export default class Header extends Component { 15 | constructor(props) { 16 | super(props); 17 | this.state = { 18 | type: props.conf.pageType, 19 | menus: props.menus, 20 | option: [ 21 | { 22 | title: this.renderFeature.bind(this), 23 | panel: true, // 这是一个面包 24 | }, 25 | { 26 | line: true, 27 | }, 28 | { 29 | title: Chrome Apps, 30 | onClick: this.openChromeApps.bind(this), 31 | }, 32 | { 33 | title: '应用选项', 34 | divider: true, 35 | line: true, 36 | }, 37 | { 38 | title: this.renderSiwtchOption.bind(this, 'isNewTab'), 39 | }, 40 | { 41 | title: this.renderSiwtchOption.bind(this, 'isHideOSC'), 42 | }, 43 | { 44 | title: this.renderSiwtchOption.bind(this, 'isHideNav'), 45 | }, 46 | { 47 | title: , 48 | divider: true, 49 | line: true, 50 | }, 51 | { 52 | title: '关于应用', 53 | divider: true, 54 | line: true, 55 | }, 56 | { 57 | title: '问题帮助', 58 | url: 'https://github.com/jaywcjlove/oscnews/issues', 59 | target: '_blank', 60 | }, 61 | { 62 | title: '应用主页', 63 | url: 'https://github.com/jaywcjlove/oscnews', 64 | target: '_blank', 65 | }, 66 | ], 67 | }; 68 | this.renderSiwtchOption = this.renderSiwtchOption.bind(this); 69 | } 70 | openChromeApps() { 71 | // eslint-disable-next-line 72 | chrome.tabs.update({ url: 'chrome://apps/' }); 73 | } 74 | renderFeature() { 75 | return ( 76 |
77 | {this.state.menus.map((item, idx) => { 78 | return ( 79 | {item.title} 80 | ); 81 | })} 82 |
83 | ); 84 | } 85 | onClickSwitch(ty, e) { 86 | e.preventDefault(); 87 | const { storage, conf } = this.props; 88 | conf[ty] = !conf[ty]; 89 | storage.set({ conf }); 90 | } 91 | renderSiwtchOption(ty) { 92 | const { conf } = this.props; 93 | let label = ''; 94 | if (ty === 'isNewTab') label = '在新标签页显示'; 95 | if (ty === 'isHideOSC') label = '隐藏新闻'; 96 | if (ty === 'isHideNav') label = '显示导航'; 97 | return ( 98 |
99 | {label} 100 | 101 |
102 | ); 103 | } 104 | onChange(type) { 105 | const { onChange } = this.props; 106 | this.setState({ type }); 107 | onChange(type); 108 | } 109 | onDropDown() { 110 | const { storage, conf } = this.props; 111 | conf.isHideNav = !conf.isHideNav; 112 | storage.set({ conf }); 113 | } 114 | render() { 115 | const { conf } = this.props; 116 | return ( 117 |
118 | {conf.isHideNav &&
} 119 |
120 | 121 | 开源中国 122 | 123 |
124 |
125 |
126 | {this.state.menus.map((item, idx) => { 127 | return ( 128 | {item.title} 129 | ); 130 | })} 131 |
132 |
133 | 134 | 135 | 136 | 137 | 138 |
139 | ); 140 | } 141 | } 142 | 143 | Header.propTypes = { 144 | onChange: PropTypes.func, 145 | }; 146 | 147 | Header.defaultProps = { 148 | onChange() {}, 149 | }; 150 | -------------------------------------------------------------------------------- /src/component/Header.module.less: -------------------------------------------------------------------------------- 1 | .warpper { 2 | padding: 0 10px; 3 | // background-color: #f9f9f9; 4 | background-color: var(--color-theme-bg); 5 | border-bottom: 1px solid var(--color-border-color); 6 | height: 50px; 7 | display: flex; 8 | align-items: center; 9 | justify-content:space-between; 10 | transition: all .3s; 11 | position: relative; 12 | z-index: 9999; 13 | img { 14 | height: 30px; 15 | } 16 | } 17 | 18 | 19 | .setting { 20 | display: inline-block; 21 | vertical-align: middle; 22 | padding: 2px 6px; 23 | position: fixed; 24 | top: 11px; 25 | right: 8px; 26 | img { 27 | height: 100%; 28 | display: block; 29 | } 30 | svg { 31 | fill: #bbb; 32 | transition: fill .3s; 33 | &:hover { 34 | fill: #333; 35 | } 36 | } 37 | .switchItem { 38 | cursor: pointer; 39 | display: flex; 40 | justify-content: space-between; 41 | } 42 | .switch { 43 | margin-left: 20px; 44 | } 45 | } 46 | 47 | .dropDown { 48 | float: right; 49 | position: absolute; 50 | margin: 34px 0 0 0; 51 | right: 20px; 52 | width: 8px; 53 | cursor: pointer; 54 | &:hover { 55 | &::after, &::before { 56 | background-color: #009801; 57 | } 58 | } 59 | &:active { 60 | &::after, &::before { 61 | background-color: #015a00; 62 | } 63 | } 64 | &::after, &::before { 65 | content: ''; 66 | display: block; 67 | background-color: #2929296b; 68 | transition: all .3s; 69 | } 70 | &::before { 71 | height: 10px; 72 | width: 2px; 73 | margin: 0 0 0 3px; 74 | } 75 | &::after { 76 | width: 8px; 77 | height: 8px; 78 | border-radius: 50%; 79 | } 80 | } 81 | 82 | .navPanel { 83 | width: 190px; 84 | span { 85 | display: inline-block; 86 | min-width: 62px; 87 | cursor: pointer; 88 | margin: 3px 0; 89 | padding: 1px 5px; 90 | border-radius: 3px; 91 | transition: all .3s; 92 | &:global(.active) { 93 | background-color: #e2e2e2; 94 | color: #333; 95 | } 96 | &:hover { 97 | background-color: #f1f1f1; 98 | } 99 | } 100 | } 101 | 102 | 103 | .nav { 104 | display: inline-block; 105 | color: #585858; 106 | user-select: none; 107 | font-size: 12px; 108 | padding: 0 30px 0 0; 109 | :global { 110 | .active, span.active:hover { 111 | color: #fff; 112 | background-color: #555; 113 | } 114 | } 115 | span, a { 116 | padding: 1px 4px; 117 | transition: all .3s; 118 | vertical-align: middle; 119 | border-radius: 3px; 120 | margin: 0 2px; 121 | transition: all .3s; 122 | &:hover { 123 | background-color: #e0e0e0; 124 | } 125 | &:active { 126 | background-color: #555; 127 | color: #fff; 128 | } 129 | } 130 | span { 131 | cursor: pointer; 132 | display: inline-block; 133 | } 134 | a { 135 | display: inline-block; 136 | img { 137 | height: 18px; 138 | display: block; 139 | } 140 | } 141 | } -------------------------------------------------------------------------------- /src/component/Icon/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const list = ( 4 | 5 | 6 | 7 | ); 8 | 9 | export { 10 | list, 11 | }; 12 | -------------------------------------------------------------------------------- /src/component/Loading/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import styles from './index.module.less'; 4 | 5 | export default function Loading({ visible, className }) { 6 | if (!visible) return null; 7 | return ( 8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/component/Loading/index.module.less: -------------------------------------------------------------------------------- 1 | 2 | @keyframes ball-spin-fade-loader { 3 | 50% { 4 | opacity: 0.3; 5 | transform: scale(0.4); 6 | } 7 | 100% { 8 | opacity: 1; 9 | transform: scale(1); 10 | } 11 | } 12 | 13 | .warpper { 14 | width: 23px; 15 | height: 23px; 16 | display: inline-block; 17 | vertical-align: middle; 18 | } 19 | 20 | 21 | .ballSpinFadeLoader { 22 | transform: scale(0.25); 23 | position: relative; 24 | top: 10px; 25 | } 26 | .ballSpinFadeLoader > div:nth-child(1) { 27 | top: 25px; 28 | left: 0; 29 | animation: ball-spin-fade-loader 1s 0s infinite linear; 30 | } 31 | .ballSpinFadeLoader > div:nth-child(2) { 32 | top: 17.04545px; 33 | left: 17.04545px; 34 | animation: ball-spin-fade-loader 1s 0.12s infinite linear; 35 | } 36 | .ballSpinFadeLoader > div:nth-child(3) { 37 | top: 0; 38 | left: 25px; 39 | animation: ball-spin-fade-loader 1s 0.24s infinite linear; 40 | } 41 | .ballSpinFadeLoader > div:nth-child(4) { 42 | top: -17.04545px; 43 | left: 17.04545px; 44 | animation: ball-spin-fade-loader 1s 0.36s infinite linear; 45 | } 46 | .ballSpinFadeLoader > div:nth-child(5) { 47 | top: -25px; 48 | left: 0; 49 | animation: ball-spin-fade-loader 1s 0.48s infinite linear; 50 | } 51 | .ballSpinFadeLoader > div:nth-child(6) { 52 | top: -17.04545px; 53 | left: -17.04545px; 54 | animation: ball-spin-fade-loader 1s 0.6s infinite linear; 55 | } 56 | .ballSpinFadeLoader > div:nth-child(7) { 57 | top: 0; 58 | left: -25px; 59 | animation: ball-spin-fade-loader 1s 0.72s infinite linear; 60 | } 61 | .ballSpinFadeLoader > div:nth-child(8) { 62 | top: 17.04545px; 63 | left: -17.04545px; 64 | animation: ball-spin-fade-loader 1s 0.84s infinite linear; 65 | } 66 | .ballSpinFadeLoader > div { 67 | background-color: #333; 68 | width: 10px; 69 | height: 10px; 70 | border-radius: 100%; 71 | margin: 2px; 72 | animation-fill-mode: both; 73 | position: absolute; 74 | } -------------------------------------------------------------------------------- /src/component/Modal/index.module.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaywcjlove/oscnews/9cd92dc9ba98247628007b57350b3d8cb71efb07/src/component/Modal/index.module.less -------------------------------------------------------------------------------- /src/component/OSCNews.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import classNames from 'classnames'; 3 | import { fetchInterval } from '../utils'; 4 | import styles from './OSCNews.module.less'; 5 | 6 | const messgeIcon = ''; 7 | 8 | export default class OSCNews extends PureComponent { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | newList: 'loading...', 13 | visible: props.visible.newBar, 14 | newType: props.conf.oscType, // '' | ndustry | project 15 | newPage: 1, 16 | newTabs: [ 17 | { 18 | title: '全部', 19 | type: '', 20 | }, { 21 | title: '综合资讯', 22 | type: 'industry', 23 | }, { 24 | title: '软件更新', 25 | type: 'project', 26 | }, 27 | ], 28 | }; 29 | } 30 | componentWillUnmount() { 31 | this.mounted = false; 32 | } 33 | componentDidMount() { 34 | this.mounted = true; 35 | this.setState({ 36 | newList: this.getNewListStore(), 37 | }); 38 | this.getNewsList(); 39 | } 40 | getNewListStore() { 41 | const { newType } = this.state; 42 | return localStorage.getItem(`osc-list${newType}`); 43 | } 44 | getNewsList() { 45 | const { newType, newPage, visible } = this.state; 46 | const { storage, conf } = this.props; 47 | const newList = this.getNewListStore(); 48 | if (!visible && newList) return; 49 | conf.oscType = newType; 50 | storage.set({ conf }); 51 | fetchInterval(`https://www.oschina.net/action/ajax/get_more_news_list?newsType=${newType}&p=${newPage}`, 1).then((response) => { 52 | if (!this.mounted) return; 53 | response = response.replace(/]+\bhref="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, (node, url, text) => { 54 | if (/^\//.test(url)) { 55 | node = `${text}`; 56 | } 57 | return node; 58 | }).replace(/]+\bsrc="([^"]*)"[^>]/gi, (node, url) => { 59 | if (/^\//.test(url)) { 60 | return node.replace(url, `https://static.oschina.net${url}`); 61 | } 62 | return node; 63 | }).replace(//gi, () => { 64 | return messgeIcon; 65 | }); 66 | this.setState({ 67 | newList: response, 68 | }, () => { 69 | localStorage.setItem(`osc-list${newType}`, response); 70 | }); 71 | }).catch(() => { 72 | if (!this.mounted) return; 73 | this.setState({ 74 | newList: this.getNewListStore() || '请求错误,请检查网路!', 75 | }); 76 | }); 77 | } 78 | onChangeTab(item) { 79 | this.setState({ 80 | newType: item.type, 81 | newPage: 1, 82 | }, () => { 83 | this.getNewsList(); 84 | }); 85 | } 86 | render() { 87 | const { newType, newTabs } = this.state; 88 | return ( 89 |
90 |
91 | {newTabs.map((item, idx) => { 92 | return ( 93 |
99 | {item.title} 100 |
101 | ); 102 | })} 103 |
104 |
105 |
106 | ); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/component/OSCNews.module.less: -------------------------------------------------------------------------------- 1 | .warpper { 2 | height: 100%; 3 | overflow: auto; 4 | :global { 5 | .active { 6 | font-weight: bold; 7 | color: #0e693c; 8 | } 9 | } 10 | .tabs { 11 | flex-direction: row; 12 | display: flex; 13 | padding: 0 10px; 14 | border-bottom: 1px solid var(--color-border-color); 15 | div { 16 | padding: 10px 5px; 17 | cursor: pointer; 18 | &:hover { 19 | color: #0e693c; 20 | } 21 | } 22 | } 23 | } 24 | 25 | .newList { 26 | font-size: 14px; 27 | padding: 10px 10px 15px; 28 | :global { 29 | .item { 30 | padding: 8px 5px; 31 | border-bottom: 1px dashed var(--color-border-color); 32 | transition: all .3s; 33 | &:hover { 34 | background: var(--color-border-color); 35 | // .summary:after { 36 | // background: linear-gradient(to right, rgba(255, 255, 255, 0), #0e1117 96%); 37 | // } 38 | } 39 | .title { 40 | font-size: 14px; 41 | line-height: 18px; 42 | text-decoration: none; 43 | font-weight: bold; 44 | display: block; 45 | transition: color .3s; 46 | &:hover { 47 | color: #0e693c; 48 | } 49 | .text-ellipsis { 50 | display: block; 51 | text-overflow: ellipsis; 52 | white-space: nowrap; 53 | overflow: hidden; 54 | position: relative; 55 | } 56 | } 57 | .summary { 58 | font-size: 12px; 59 | max-height: 2.6em; 60 | margin: 8px 0; 61 | position: relative; 62 | overflow: hidden; 63 | &:after { 64 | position: absolute; 65 | bottom: 0; 66 | content: ''; 67 | right: 0; 68 | display: block; 69 | width: 160px; 70 | height: 20px; 71 | // background: linear-gradient(to right, rgba(255, 255, 255, 0), #fff 96%); 72 | } 73 | } 74 | .thumb { 75 | max-width: 150px; 76 | margin-top: -38px; 77 | margin-left: 11px; 78 | position: relative; 79 | z-index: 9; 80 | float: right; 81 | img { 82 | max-height: 40px; 83 | overflow: hidden; 84 | } 85 | } 86 | .from { 87 | a, img, span, svg:not([class~=hide]) { 88 | display: inline-block; 89 | vertical-align: middle; 90 | } 91 | svg { 92 | width: 14px !important; 93 | height: 12px !important; 94 | vertical-align: middle; 95 | path { 96 | fill: #868686; 97 | } 98 | } 99 | .avatar { 100 | overflow: hidden; 101 | display: block; 102 | width: 14px; 103 | min-width: 14px; 104 | max-width: 14px; 105 | height: 14px; 106 | border-radius: 12px; 107 | } 108 | .mr { 109 | margin-right: 15px; 110 | font-size: .75rem; 111 | color: #9d9d9d; 112 | font-weight: normal; 113 | vertical-align: middle; 114 | a { 115 | vertical-align: initial; 116 | color: #a0a0a0; 117 | &:hover { 118 | color: #0e693c; 119 | } 120 | } 121 | } 122 | a { 123 | text-decoration: none; 124 | } 125 | } 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/component/Progress/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styles from './index.module.less'; 4 | 5 | export default class Progress extends Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = {}; 9 | } 10 | render() { 11 | const { percent, ...resetProps } = this.props; 12 | const percentStyle = { 13 | width: `${percent}%`, 14 | }; 15 | return ( 16 |
17 |
18 |
19 |
20 |
21 | ); 22 | } 23 | } 24 | 25 | Progress.propTypes = { 26 | percent: PropTypes.number.isRequired, 27 | }; 28 | 29 | Progress.defaultProps = { 30 | percent: 0, // 百分比(必填) 31 | }; 32 | -------------------------------------------------------------------------------- /src/component/Progress/index.module.less: -------------------------------------------------------------------------------- 1 | .progress { 2 | height: 15px; 3 | display: flex; 4 | } 5 | 6 | .inner { 7 | background-color: #f1f1f1; 8 | width: 100%; 9 | height: 100%; 10 | border-radius: 3px; 11 | position: relative; 12 | margin-right: 5px; 13 | .bar { 14 | height: 100%; 15 | width: 0; 16 | background-color: #6ec382; 17 | transition: all .3s; 18 | border-radius: 3px; 19 | } 20 | } -------------------------------------------------------------------------------- /src/component/Search/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import classNames from 'classnames'; 3 | import styles from './index.module.less'; 4 | import Select from '../Select'; 5 | 6 | export default class Search extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = {}; 10 | } 11 | render() { 12 | const { 13 | className, 14 | placeholder, 15 | autoComplete = 'off', 16 | // autoFocus, 17 | select, 18 | query, 19 | onChange, 20 | onKeyUp, 21 | onSearch, 22 | style, 23 | inputStyle, 24 | } = this.props; 25 | 26 | return ( 27 |
28 |
29 | {select && this.input = input} placeholder={placeholder} autoComplete={autoComplete} style={inputStyle} value={query} onChange={onChange} onKeyUp={onKeyUp} /> 31 |
32 |
33 | 34 |
35 |
36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/component/Search/index.module.less: -------------------------------------------------------------------------------- 1 | 2 | .search { 3 | position: relative; 4 | display: table; 5 | border-collapse: separate; 6 | margin: 0 auto; 7 | .input { 8 | display: flex; 9 | align-items: center; 10 | background-color: var(--color-active); 11 | background-image: none; 12 | border: 1px solid var(--color-border-color); 13 | border-radius: 5px 0 0 5px; 14 | padding: 0 0 0 7px; 15 | &.noSelect { 16 | padding: 0; 17 | input { 18 | border-radius: 5px 0 0 5px; 19 | } 20 | } 21 | } 22 | input { 23 | resize: none; 24 | position: relative; 25 | z-index: 2; 26 | width: 100%; 27 | height: 38px; 28 | padding: 6px 10px; 29 | font-size: 14px; 30 | font-weight: bold; 31 | line-height: 1.42857143; 32 | // color: #555; 33 | border: 0; 34 | display: table-cell; 35 | transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; 36 | &:focus { 37 | z-index: 9; 38 | border: 0; 39 | outline: 0; 40 | } 41 | &::placeholder { 42 | transition: all .3s; 43 | } 44 | &:hover::placeholder { 45 | color: #d4d4d4; 46 | } 47 | &::placeholder { 48 | font-weight: normal; 49 | } 50 | &:focus::placeholder { 51 | color: #d4d4d4; 52 | } 53 | } 54 | .searchBtn { 55 | width: 1%; 56 | white-space: nowrap; 57 | vertical-align: middle; 58 | display: table-cell; 59 | button { 60 | margin: 0; 61 | left: -1px; 62 | position: relative; 63 | z-index: 5; 64 | display: inline-block; 65 | padding: 9px 23px; 66 | font-size: 14px; 67 | font-weight: bold; 68 | line-height: 1.42857143; 69 | text-align: center; 70 | text-rendering: auto; 71 | white-space: nowrap; 72 | vertical-align: middle; 73 | touch-action: manipulation; 74 | cursor: pointer; 75 | user-select: none; 76 | background-image: none; 77 | background-color: var(--color-active); 78 | border: 1px solid transparent; 79 | border-collapse: separate; 80 | border-radius: 0 5px 5px 0; 81 | border-color: var(--color-border-color); 82 | // color: #333; 83 | box-shadow: inset 0 0 0 rgba(0,0,0,0.075), 0 0 0 0 rgba(102,175,233,0.5); 84 | transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out; 85 | &:hover, &:active, &:focus { 86 | color: var(--color-theme-text); 87 | // background-color: #e6e6e6; 88 | } 89 | &:hover { 90 | border-color: var(--color-border-color); 91 | } 92 | &:active { 93 | border-color: var(--color-border-color); 94 | background-image: none; 95 | } 96 | &:focus { 97 | outline: 0; 98 | border-color: var(--color-border-color); 99 | text-decoration: none; 100 | box-shadow: inset 0 1px 1px rgba(0,0,0,0.075), 0 0 0 2px rgba(102,175,233,0.3); 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/component/Select/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import classNames from 'classnames'; 4 | import PropTypes from 'prop-types'; 5 | import styles from './index.module.less'; 6 | 7 | export default class Select extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | visible: false, 12 | option: props.option, 13 | value: props.value, 14 | }; 15 | this.handleClickOutside = this.handleClickOutside.bind(this); 16 | } 17 | componentDidMount() { 18 | this.setDefaultValue(); 19 | document.addEventListener('mousedown', this.handleClickOutside, true); 20 | } 21 | setDefaultValue() { 22 | const { value, option } = this.state; 23 | if (!value && option && option.length > 0) { 24 | this.setState({ 25 | value: option[0].value || '', 26 | }); 27 | } 28 | } 29 | componentWillUnmount() { 30 | document.removeEventListener('mousedown', this.handleClickOutside, true); 31 | } 32 | componentDidUpdate(prevProps, prevState) { 33 | if (prevProps.option !== this.props.option) { 34 | this.setState({ option: this.props.option }); 35 | } 36 | if (prevProps.value !== this.props.value) { 37 | this.setState({ value: this.props.value }); 38 | } 39 | } 40 | handleClickOutside(e) { 41 | // Ignore clicks on the component it self 42 | // https://codepen.io/graubnla/pen/EgdgZm 43 | // Detect a click outside of a React Component 44 | // https://www.dhariri.com/posts/57c724e4d1befa66e5b8e056 45 | const domNode = ReactDOM.findDOMNode(this); 46 | if ((!domNode || !domNode.contains(e.target))) { 47 | this.setState({ visible: false }); 48 | } 49 | } 50 | onClick() { 51 | this.setState({ 52 | visible: !this.state.visible, 53 | }); 54 | } 55 | onSelect(item) { 56 | const { onSelect } = this.props; 57 | this.setState({ 58 | value: item.value, 59 | visible: !this.state.visible, 60 | }, () => { 61 | onSelect(item, item.value); 62 | }); 63 | } 64 | onFilterLang(e) { 65 | const { value } = e.target; 66 | const { option } = this.state; 67 | const filterData = option.filter(item => item.value.toLowerCase().indexOf(value.toLowerCase()) > -1); 68 | this.setState({ 69 | option: value.length > 0 ? filterData : this.props.option, 70 | }); 71 | } 72 | renderItems(items, value) { 73 | return items.map((item, idx) => { 74 | return ( 75 |
82 | {item.label} 83 |
84 | ); 85 | }); 86 | } 87 | render() { 88 | const { showSearch, className, suggest, optionStyle, placeholder, ...resetProps } = this.props; 89 | const { visible, value, option } = this.state; 90 | const title = this.props.option.filter(item => item.value === value); 91 | const suggestItems = this.props.option.filter(item => suggest.includes(item.value)); 92 | delete resetProps.option; 93 | delete resetProps.onSelect; 94 | return ( 95 |
101 |
102 | {title && title.length > 0 && title[0].label} 103 |
104 |
105 | {showSearch && ( 106 |
107 | 108 |
109 | )} 110 |
111 | {this.renderItems(suggestItems, value)} 112 | {suggestItems.length > 0 &&
} 113 | {this.renderItems(option, value)} 114 |
115 |
116 |
117 | ); 118 | } 119 | } 120 | 121 | Select.propsTypes = { 122 | value: PropTypes.string, 123 | placeholder: PropTypes.string, 124 | option: PropTypes.array, 125 | suggest: PropTypes.array, 126 | showSearch: PropTypes.bool, 127 | onSelect: PropTypes.func, 128 | }; 129 | 130 | Select.defaultProps = { 131 | option: [], 132 | suggest: [], 133 | value: '', 134 | placeholder: '请输入文本', 135 | showSearch: false, 136 | onSelect() {}, 137 | }; 138 | -------------------------------------------------------------------------------- /src/component/Select/index.module.less: -------------------------------------------------------------------------------- 1 | 2 | 3 | .select { 4 | position: relative; 5 | display: inline-block; 6 | line-height: 25px; 7 | & + .select { 8 | margin: 0 0 0 10px; 9 | } 10 | &:global(.hide) { 11 | .option { 12 | opacity: 0; 13 | z-index: -1; 14 | } 15 | } 16 | &:global(.visible) { 17 | .title { 18 | border-color: rgba(27,31,35,0.35); 19 | box-shadow: inset 0 0.15em 0.3em rgba(27,31,35,0.15); 20 | &::after { 21 | transform: rotate(180deg) 22 | } 23 | } 24 | .option { 25 | opacity: 1; 26 | z-index: 9; 27 | } 28 | } 29 | :global { 30 | .selected { 31 | &::before { 32 | content: ''; 33 | display: block; 34 | float: left; 35 | height: 6px; 36 | width: 3px; 37 | border-bottom: 1px solid #333; 38 | border-right: 1px solid #333; 39 | transform: rotate(30deg); 40 | position: absolute; 41 | left: 5px; 42 | top: 8px; 43 | } 44 | } 45 | } 46 | } 47 | 48 | .title { 49 | white-space: nowrap; 50 | background-color: transparent; 51 | padding: 0 10px; 52 | border-radius: 3px; 53 | cursor: pointer; 54 | &::after { 55 | content: ''; 56 | display: inline-block; 57 | transition: all .3s; 58 | width: 0; 59 | height: 0; 60 | border-top: 4px solid #333; 61 | border-right: 4px solid transparent; 62 | border-left: 4px solid transparent; 63 | margin-left: 5px; 64 | margin-top: -2px; 65 | vertical-align: middle; 66 | } 67 | } 68 | 69 | .search { 70 | border-bottom: 1px solid #dedede; 71 | padding: 4px 5px; 72 | input { 73 | font-size: 12px; 74 | line-height: 10px; 75 | color: #24292e; 76 | background-color: #fff; 77 | background-repeat: no-repeat; 78 | background-position: right 8px center; 79 | outline: none; 80 | box-shadow: inset 0 1px 2px rgba(27,31,35,0.075); 81 | 82 | display: block; 83 | width: 100%; 84 | max-width: 100%; 85 | padding: 5px; 86 | border: 1px solid #dfe2e5; 87 | border-radius: 3px; 88 | &:focus { 89 | border-color: #0366d8; 90 | outline: none; 91 | box-shadow: inset 0 1px 2px rgba(27,31,35,0.075), 0 0 0 0.2em rgba(3,102,214,0.1); 92 | } 93 | } 94 | } 95 | 96 | .option { 97 | position: absolute; 98 | min-width: 120px; 99 | margin-top: 4px; 100 | font-size: 12px; 101 | color: #586069; 102 | background-color: #fff; 103 | background-clip: padding-box; 104 | border: 1px solid rgba(27,31,35,0.15); 105 | border-radius: 3px; 106 | box-shadow: 0 3px 12px rgba(27,31,35,0.15); 107 | transition: all .3s; 108 | .divider { 109 | border-bottom: 1px solid #e4e4e4; 110 | } 111 | .optionList { 112 | max-height: 230px; 113 | overflow: auto; 114 | &::before, &::after { 115 | content: ''; 116 | display: block; 117 | height: 3px; 118 | } 119 | } 120 | :global { 121 | .item { 122 | white-space:nowrap; 123 | padding: 0 10px 0 15px; 124 | position: relative; 125 | cursor: pointer; 126 | &:hover { 127 | background-color: #e8e8e8; 128 | } 129 | } 130 | } 131 | } -------------------------------------------------------------------------------- /src/component/Switch/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import classNames from 'classnames'; 3 | import styles from './index.module.less'; 4 | 5 | export default class Switch extends Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | _checked: props.checked || false, 10 | }; 11 | this.onChange = this.onChange.bind(this); 12 | } 13 | componentDidUpdate(prevProps, prevState) { 14 | if (this.props.checked !== prevProps.checked) { 15 | this.setState({ _checked: this.props.checked }); 16 | } 17 | } 18 | onChange(e) { 19 | const { onChange } = this.props; 20 | this.setState({ 21 | _checked: e.target.checked, 22 | }); 23 | onChange && onChange(e, e.target.checked); 24 | } 25 | render() { 26 | const { className, disabled, style, ...resetProps } = this.props; 27 | const { _checked } = this.state; 28 | const cls = classNames(className, styles.switch, { 29 | [`${styles.disabled}`]: disabled, 30 | [`${styles.checked}`]: _checked, 31 | }); 32 | return ( 33 | 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/component/Switch/index.module.less: -------------------------------------------------------------------------------- 1 | 2 | @switch-height: 24px; 3 | @switch-width: 45px; 4 | 5 | .switch { 6 | position: relative; 7 | border: 1px solid #dfdfdf; 8 | vertical-align: middle; 9 | line-height: 17px; 10 | min-width: 35px; 11 | height: 20px; 12 | font-size: 12px; 13 | padding-left: 18px; 14 | padding-right: 4px; 15 | outline: 0; 16 | border-radius: 16px; 17 | box-sizing: border-box; 18 | background-color: #dfdfdf; 19 | display: inline-block; 20 | transition: background-color .2s ease-in, min-width .2s; 21 | user-select: none; 22 | overflow: hidden; 23 | input { 24 | position: relative; 25 | z-index: -1; 26 | left: -99999px; 27 | display: none; 28 | } 29 | &:before,&:after { 30 | content: " "; 31 | position: absolute; 32 | top: 0; 33 | left: 1px; 34 | border-radius: 50%; 35 | transition: all .3s; 36 | } 37 | &:before { 38 | width: @switch-width - 29; 39 | height: @switch-height - 8; 40 | } 41 | &:after { 42 | width: @switch-height - 8; 43 | height: @switch-height - 8; 44 | background-color: #fff; 45 | top: 1px; 46 | } 47 | &.disabled { 48 | opacity: .7; 49 | cursor: not-allowed; 50 | } 51 | &.checked { 52 | border-color: #04be02; 53 | background-color: #04be02; 54 | padding-left: 4px; 55 | padding-right: 18px; 56 | &:before { 57 | transform: scale(0); 58 | } 59 | &:after { 60 | left: 100%; 61 | margin-left: -17px; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/component/container/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import classNames from 'classnames'; 3 | import Header from '../Header'; 4 | import OSCNews from '../OSCNews'; 5 | import styles from './index.module.less'; 6 | 7 | export default class Container extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | type: props.config.conf.pageType, 12 | visible: props.config.visible.newBar, 13 | siderBarWidth: props.config.conf.siderBarWidth, 14 | }; 15 | this.onDragging = this.onDragging.bind(this); 16 | this.onDragEnd = this.onDragEnd.bind(this); 17 | } 18 | onSwitchBtn() { 19 | const { storage, visible } = this.props.config; 20 | visible.newBar = !this.state.visible; 21 | storage.set({ visible }, () => { 22 | this.setState({ 23 | visible: !this.state.visible, 24 | }); 25 | }); 26 | } 27 | onDragEnd() { 28 | const { storage, conf } = this.props.config; 29 | document.body.style.userSelect = 'text'; 30 | conf.siderBarWidth = this.siderBar.clientWidth; 31 | storage.set({ conf }, () => { 32 | this.setState({ 33 | siderBarWidth: this.siderBar.clientWidth, 34 | }); 35 | }); 36 | window.removeEventListener('mousemove', this.onDragging, true); 37 | window.removeEventListener('mouseup', this.onDragEnd, true); 38 | } 39 | onDragging(e) { 40 | const currentX = e.clientX; 41 | if (this.siderBar && this.barWidth) { 42 | let width = this.barWidth + (currentX - this.startX); 43 | if (width < 200) width = 200; 44 | this.siderBar.style.width = `${width}px`; 45 | } 46 | } 47 | onMouseDown(event) { 48 | this.startX = event.clientX; 49 | this.startY = event.clientY; 50 | this.barWidth = this.siderBar.clientWidth; 51 | document.body.style.userSelect = 'none'; 52 | window.addEventListener('mousemove', this.onDragging, true); 53 | window.addEventListener('mouseup', this.onDragEnd, true); 54 | } 55 | render() { 56 | const { children, config } = this.props; 57 | const { visible, siderBarWidth } = this.state; 58 | const OSCNewsLeft = () => { 59 | if (config.conf.isHideOSC) return visible ? 0 : -siderBarWidth; 60 | return -siderBarWidth; 61 | }; 62 | return ( 63 |
64 |
65 |
{ 69 | const { conf, storage } = config; 70 | conf.pageType = type; 71 | storage.set({ conf }, () => { 72 | this.setState({ type }); 73 | }); 74 | }} 75 | /> 76 |
77 |
78 |
this.siderBar = node} className={styles.oscnews} style={{ marginLeft: OSCNewsLeft(), width: siderBarWidth }}> 79 | {config.conf.isHideOSC && ( 80 |
81 | {visible ? '隐藏' : '显示新闻'} 82 |
83 | )} 84 | 85 |
89 |
90 |
91 | {React.Children.map(children, (Child) => { 92 | if (!Child || Child.type.typeName !== this.state.type) return null; 93 | return React.cloneElement(Child, { 94 | contentType: this.state.type, 95 | ...config, 96 | }); 97 | })} 98 |
99 |
100 |
101 | ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/component/container/index.module.less: -------------------------------------------------------------------------------- 1 | .warpper { 2 | display: flex; 3 | width: 100%; 4 | height: 100vh; 5 | flex-direction: column; 6 | } 7 | 8 | .content { 9 | width: 100%; 10 | height: 100%; 11 | flex: 1; 12 | overflow: auto; 13 | flex-direction: row; 14 | display: flex; 15 | } 16 | 17 | .oscnews { 18 | height: 100%; 19 | width: 360px; 20 | position: relative; 21 | transition: margin-left .3s; 22 | } 23 | 24 | .trending { 25 | height: 100%; 26 | overflow: auto; 27 | flex: 2; 28 | } 29 | 30 | .switchBtn { 31 | position: absolute; 32 | z-index: 99; 33 | right: 0; 34 | margin: 10px 10px 0 0; 35 | transition: all .6s; 36 | line-height: 14px; 37 | background-color: #717171; 38 | color: #fff; 39 | padding: 3px; 40 | border-radius: 2px; 41 | font-size: 12px; 42 | cursor: pointer; 43 | &:hover { 44 | background-color: #008a05; 45 | } 46 | &:global(.hidden) { 47 | right: -26px; 48 | top: 57px; 49 | opacity: .3; 50 | color: #fff; 51 | background-color: #444; 52 | width: 16px; 53 | text-align: center; 54 | padding: 4px 2px 4px 0px; 55 | border-radius: 0 3px 3px 0; 56 | &:hover { 57 | opacity: 1; 58 | } 59 | } 60 | } 61 | 62 | .newsBar { 63 | cursor: e-resize; 64 | position: absolute; 65 | width: 3px; 66 | height: 100%; 67 | top: 0; 68 | right: -3px; 69 | z-index: 99; 70 | &:hover { 71 | background-color: #e4e4e4; 72 | } 73 | } -------------------------------------------------------------------------------- /src/component/modal/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './index.module.less'; 3 | 4 | export default class Modal extends Component { 5 | render() { 6 | return ( 7 |
8 |
9 |
10 |
11 | ) 12 | } 13 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Root from './Root'; 4 | import './index.less'; 5 | 6 | // eslint-disable-next-line 7 | const storage = chrome.storage.sync; 8 | 9 | storage.get(['oscconfig', 'visible', 'conf', 'dbs', 'todo'], (items) => { 10 | // 默认顶部菜单,和新闻是否展示判断 11 | if (!items.visible) items.visible = {}; 12 | if (items.visible.header === undefined) items.visible.header = true; 13 | if (items.visible.newBar === undefined) items.visible.newBar = true; 14 | // 数据存储 15 | if (!items.dbs) items.dbs = {}; 16 | if (items.dbs.nav === undefined) items.dbs.nav = []; 17 | 18 | // 默认清单(Todo)数据内容 19 | if (!items.todo) items.todo = {}; 20 | if (items.todo.active < 0) items.todo.active = 0; 21 | if (!items.todo.list) { 22 | items.todo.list = [ 23 | { label: '旅游', list: [] }, 24 | { label: '私人', list: [] }, 25 | { label: '家庭', list: [] }, 26 | { 27 | label: '工作', 28 | list: [ 29 | { 30 | id: new Date().getTime(), // TODO ID 31 | task: '欢迎使用 TODO 任务清单!', // 任务描述 32 | complete: false, // 完成状态 33 | comment: '这里放任务注释!', // 注释 34 | star: false, // 是否收藏 35 | }, 36 | { 37 | id: (new Date().getTime()) + 1, // TODO ID 38 | task: '开发 TODO 功能!', // 任务描述 39 | complete: true, // 完成状态 40 | comment: '这里放任务注释!', // 注释 41 | star: false, // 是否收藏 42 | }, 43 | ], 44 | }, 45 | ]; 46 | } 47 | 48 | // 默认选中的栏目 49 | if (!items.conf) items.conf = {}; 50 | // 默认是否在新标签页显示 51 | if (items.conf.isNewTab === undefined) items.conf.isNewTab = true; 52 | // 默认是否在界面上展示 OSC 新闻,包括隐藏按钮 53 | if (items.conf.isHideOSC === undefined) items.conf.isHideOSC = true; 54 | // 默认是否隐藏导航 55 | if (items.conf.isHideNav === undefined) items.conf.isHideNav = true; 56 | // 默认是否在新标签页显示 57 | if (items.conf.historyTabType === undefined) items.conf.historyTabType = 'today'; 58 | // 默认展示页面 59 | if (!items.conf.pageType) items.conf.pageType = 'document'; 60 | // 默认新闻展示tab类型 61 | if (!items.conf.oscType) items.conf.oscType = ''; 62 | // 默认新闻展示宽度设置 63 | if (!items.conf.siderBarWidth) items.conf.siderBarWidth = 360; 64 | // 开发文档导航设置 65 | if (!items.conf.docTag) items.conf.docTag = ''; 66 | if (!items.conf.docStar) items.conf.docStar = []; 67 | // 空白页背景颜色 68 | if (!items.conf.BlankColor) items.conf.BlankColor = 'clouds'; 69 | // GitHub趋势榜设置 70 | if (!items.conf.githubSince) items.conf.githubSince = ''; 71 | if (!items.conf.githubLang) items.conf.githubLang = ''; 72 | // 搜索默认选中设置 73 | if (!items.conf.selectType) items.conf.selectType = 'web'; 74 | if (!items.conf.selectSubType) items.conf.selectSubType = ''; // 为空默认数组第一个 75 | 76 | items.storage = storage; 77 | if (!/#normal$/.test(window.location.hash) && items.conf.isNewTab === false) { 78 | // eslint-disable-next-line 79 | chrome.tabs.update({ url: 'chrome-search://local-ntp/local-ntp.html' }); 80 | } else { 81 | ReactDOM.render( 82 | , 83 | document.getElementById('root') 84 | ); 85 | } 86 | }); 87 | -------------------------------------------------------------------------------- /src/index.less: -------------------------------------------------------------------------------- 1 | body, html { 2 | margin: 0; 3 | padding: 0; 4 | height: 100%; 5 | width: 100%; 6 | font-size: 12px; 7 | } 8 | ul, li { 9 | list-style: none; 10 | margin: 0; 11 | padding: 0; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | [data-color-mode*='dark'], [data-color-mode*='dark'] body { 19 | --color-border-color: #ffffff12; 20 | --color-active: #ffffff0d; 21 | } 22 | 23 | [data-color-mode*='light'], [data-color-mode*='light'] body { 24 | --color-border-color: #0000001f; 25 | --color-active: #00000012; 26 | } -------------------------------------------------------------------------------- /src/pages/Blank.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import classNames from 'classnames'; 4 | import solarLunar from 'solarlunar'; 5 | import { theWeek } from '../utils'; 6 | import styles from './Blank.module.less'; 7 | import Clock from '../component/Clock'; 8 | 9 | // 搜索地点 10 | // http://toy1.weather.com.cn/search?cityname=%E5%8C%97%E4%BA%AC 11 | // 根据id查询天气 12 | // http://www.weather.com.cn/data/sk/101010300.html 13 | 14 | const time = () => { 15 | const hours = new Date().getHours(); 16 | const minutes = new Date().getMinutes(); 17 | const seconds = new Date().getSeconds(); 18 | return ( 19 |
20 | {`${hours < 10 ? `0${hours}` : hours}`} 21 | {`${minutes < 10 ? `0${minutes}` : minutes}`} 22 | {`${seconds < 10 ? `0${seconds}` : seconds}`} 23 |
24 | ); 25 | }; 26 | 27 | const date = () => { 28 | return ( 29 |
30 | {`${new Date().getMonth() + 1}月${new Date().getDate()}日 ${['周日', '周一', '周二', '周三', '周四', '周五', '周六'][new Date().getDay()]} `} 31 | {`第${theWeek()}周`} 32 |
33 | ); 34 | }; 35 | 36 | const colorFill = ( 37 | 38 | 39 | 40 | ); 41 | 42 | export default class Blank extends Component { 43 | static typeName = 'blank' 44 | constructor(props) { 45 | super(props); 46 | this.state = { 47 | time: time(), 48 | date: date(), 49 | color: [ 50 | { color: '#34495e', lable: 'wet asphalt', value: 'wet-asphalt' }, 51 | { color: '#2ecc71', lable: 'emerald', value: 'emerald' }, 52 | { color: '#1abc9c', lable: 'turquoise', value: 'turquoise' }, 53 | { color: '#f1c40f', lable: 'sun flower', value: 'sun-flower' }, 54 | { color: '#e74c3c', lable: 'alizarin', value: 'alizarin' }, 55 | { color: '#f1f1f1', lable: 'clouds', value: 'clouds' }, 56 | { color: '#3498db', lable: 'peter river', value: 'peter-river' }, 57 | { color: '#9b59b6', lable: 'amethyst', value: 'amethyst' }, 58 | ], 59 | bgName: props.conf.BlankColor, 60 | colorVisibel: false, 61 | }; 62 | this.handleClickOutside = this.handleClickOutside.bind(this); 63 | } 64 | componentWillUnmount() { 65 | if (this.interval) clearInterval(this.interval); 66 | document.removeEventListener('mousedown', this.handleClickOutside, true); 67 | } 68 | componentDidMount() { 69 | document.addEventListener('mousedown', this.handleClickOutside, true); 70 | this.interval = setInterval(() => { 71 | this.setState({ 72 | time: time(), 73 | date: date(), 74 | }); 75 | }, 1000); 76 | } 77 | handleClickOutside(e) { 78 | // Ignore clicks on the component it self 79 | // https://codepen.io/graubnla/pen/EgdgZm 80 | // Detect a click outside of a React Component 81 | // https://www.dhariri.com/posts/57c724e4d1befa66e5b8e056 82 | const domNode = ReactDOM.findDOMNode(this); 83 | if (!domNode || !domNode.contains(e.target) || domNode === e.target) { 84 | this.setState({ colorVisibel: false }); 85 | } 86 | } 87 | onClick(item) { 88 | const { storage, conf } = this.props; 89 | conf.BlankColor = item.value; 90 | storage.set({ conf }); 91 | this.setState({ bgName: item.value }); 92 | } 93 | onClickColorPanel() { 94 | const { colorVisibel } = this.state; 95 | this.setState({ colorVisibel: !colorVisibel }); 96 | } 97 | render() { 98 | const { color, bgName, colorVisibel } = this.state; 99 | const lunar = solarLunar.solar2lunar(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()); 100 | return ( 101 |
102 |
103 | 104 | {this.state.time} 105 | {this.state.date} 106 |
107 | 108 | 农历{lunar.lYear}年 {lunar.monthCn} {lunar.dayCn} {lunar.animal}年 {lunar.term} 109 | 110 |
111 | {lunar.gzYear}年 {lunar.gzMonth}月 {lunar.gzDay}日 112 |
113 |
114 |
115 |
116 |
121 | {color.map((item, key) => ( 122 |
128 | ))} 129 |
130 |
{colorFill}
131 |
132 |
133 | ); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/pages/Blank.module.less: -------------------------------------------------------------------------------- 1 | 2 | 3 | .warpper { 4 | display: flex; 5 | align-items: center; 6 | height: 100%; 7 | user-select: none; 8 | } 9 | 10 | .calendar { 11 | font-size: 48px; 12 | margin-left: 200px; 13 | font-weight: 100; 14 | text-align: left; 15 | .minutes { 16 | &::before, &::after { 17 | content: ':'; 18 | display: inline-block; 19 | position: relative; 20 | top: -3px; 21 | padding: 0 3px; 22 | } 23 | &::after { 24 | font-size: 30px; 25 | font-weight: 200; 26 | top: -5px; 27 | } 28 | } 29 | .seconds { 30 | font-size: 30px; 31 | position: relative; 32 | top: -3px; 33 | } 34 | .date { 35 | font-size: 24px; 36 | margin: 10px 0 0 0; 37 | sup { 38 | font-size: 12px; 39 | background-color: #00000030; 40 | padding: 1px 3px; 41 | border-radius: 2px; 42 | } 43 | } 44 | .lunar { 45 | font-size: 14px; 46 | background-color: #00000014; 47 | padding: 3px 6px; 48 | border-radius: 3px; 49 | } 50 | .lunarGz { 51 | font-size: 12px; 52 | } 53 | } 54 | 55 | 56 | .setting { 57 | position: absolute; 58 | bottom: 10px; 59 | right: 10px; 60 | .panel { 61 | height: 79px; 62 | width: 142px; 63 | display: flex; 64 | flex-wrap: wrap; 65 | justify-content: center; 66 | align-content: center; 67 | position: absolute; 68 | right: 0; 69 | bottom: 40px; 70 | background-color: #fff; 71 | background-clip: padding-box; 72 | border: 1px solid rgba(27, 31, 35, 0.15); 73 | border-radius: 3px; 74 | box-shadow: 0 3px 12px rgba(27, 31, 35, 0.15); 75 | transition: all .3s; 76 | .item { 77 | height: 32px; 78 | width: 32px; 79 | text-align: center; 80 | color: #fff; 81 | } 82 | &:global { 83 | &.hiden { 84 | display: none; 85 | } 86 | &.show { 87 | display: flex; 88 | } 89 | } 90 | } 91 | .btn { 92 | height: 32px; 93 | width: 32px; 94 | cursor: pointer; 95 | display: flex; 96 | justify-content: center; 97 | align-items: center; 98 | svg { 99 | fill: #00000040; 100 | } 101 | &:hover svg{ 102 | fill: #00000080; 103 | } 104 | } 105 | } 106 | 107 | .warpper { 108 | &:global { 109 | &.wet-asphalt { background-color: #34495e;color: #fff;} 110 | &.emerald { background-color: #2ecc71;color: #fff;} 111 | &.turquoise { background-color: #1abc9c;color: #fff;} 112 | &.sun-flower { background-color: #f1c40f;color: #fff;} 113 | &.alizarin { background-color: #e74c3c;color: #fff;} 114 | // &.clouds { background-color: #f1f1f1;} 115 | &.peter-river { background-color: #3498db;color: #fff;} 116 | &.amethyst { background-color: #9b59b6;color: #fff;} 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/pages/Document/icons.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './index.module.less'; 3 | 4 | const github = ( 5 | 6 | 7 | 8 | ); 9 | const zhHans = ( 10 | 11 | 12 | 13 | 14 | ); 15 | 16 | const heart = ( 17 | 18 | 19 | 20 | ); 21 | 22 | const website = ( 23 | 24 | 25 | 26 | ); 27 | 28 | export { 29 | github, 30 | zhHans, 31 | heart, 32 | website, 33 | }; 34 | -------------------------------------------------------------------------------- /src/pages/Document/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import classNames from 'classnames'; 3 | import styles from './index.module.less'; 4 | import Footer from '../../component/Footer'; 5 | import source from '../../dev-site/src/document.json'; 6 | import { github, zhHans, heart, website } from './icons'; 7 | 8 | if (!localStorage.getItem('osc-doc')) { 9 | localStorage.setItem('osc-doc', JSON.stringify(source)); 10 | } 11 | 12 | 13 | export default class DevDocument extends Component { 14 | static typeName = 'document' 15 | constructor(props) { 16 | super(props); 17 | this.state = { 18 | lists: [], 19 | star: props.conf.docStar, 20 | tag: props.conf.docTag, 21 | query: '', 22 | subMenu: [ 23 | { title: '我的收藏', tag: '__star__' }, 24 | { title: '全部', tag: '' }, 25 | { title: '前端', tag: '前端' }, 26 | { title: '后端', tag: '后端' }, 27 | { title: '工具', tag: '工具库' }, 28 | ], 29 | }; 30 | } 31 | componentDidMount() { 32 | const docs = localStorage.getItem('osc-doc'); 33 | if (!docs) { 34 | localStorage.setItem('osc-doc', JSON.stringify(source)); 35 | } 36 | this.setState({ 37 | lists: source, 38 | query: '', 39 | }); 40 | } 41 | onAddStar(title) { 42 | const { star } = this.state; 43 | const { storage, conf } = this.props; 44 | if (star.indexOf(title) === -1) { 45 | star.push(title); 46 | } else { 47 | star.splice(star.indexOf(title), 1); 48 | } 49 | conf.docStar = star; 50 | storage.set({ conf }, () => { 51 | this.setState({ star }); 52 | }); 53 | } 54 | onChangeTag(tag) { 55 | const { storage, conf } = this.props; 56 | conf.docTag = tag; 57 | this.setState({ tag, query: '' }, () => { 58 | storage.set({ conf }); 59 | }); 60 | } 61 | onSearch(e) { 62 | const query = e.target.value; 63 | this.setState({ query }); 64 | } 65 | getFilterLists() { 66 | const { query, lists } = this.state; 67 | return !query ? lists : lists.filter(item => item.title.toLowerCase().indexOf(query.toLowerCase()) > -1); 68 | } 69 | render() { 70 | const lists = this.getFilterLists(); 71 | return ( 72 |
73 |
74 | 开发文档 75 |
76 | {!this.state.tag && } 77 | {this.state.subMenu.map((item, idx) => { 78 | return ( 79 | 86 | {item.title} 87 | 88 | ); 89 | })} 90 |
91 |
92 | {this.state.star.length === 0 && this.state.tag === '__star__' &&
还没有收藏,赶紧去收藏吧
} 93 |
    94 | {lists.map((item, idx) => { 95 | const urls = []; 96 | for (const i in item.urls) { 97 | if (Object.prototype.hasOwnProperty.call(item.urls, i)) { 98 | let icon = ''; 99 | if (i === 'git') icon = github; 100 | else if (i === 'cn') icon = zhHans; 101 | else icon = website; 102 | urls.push( 103 | {icon} 104 | ); 105 | } 106 | } 107 | 108 | const { tag } = this.state; 109 | const isTag = item.tags.filter(t => t === tag); 110 | const isStar = this.state.star.filter(t => t === item.title); 111 | 112 | if (tag === '' || (tag === '__star__' && isStar.length > 0) || isTag.length > 0) { 113 | return ( 114 |
  • 115 | 116 |
    117 | {item.title &&

    {item.title}

    } 118 | {item.logo && {item.title}} 119 |
    120 |
    121 | {item.des} 122 |
    123 |
    124 |
    125 |
    {urls}
    126 |
    -1, 129 | })} 130 | onClick={this.onAddStar.bind(this, item.title)} 131 | > 132 | {heart} 133 |
    134 |
    135 |
  • 136 | ); 137 | } 138 | return null; 139 | })} 140 |
141 |
Copyright © 2018
142 |
143 | ); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/pages/Document/index.module.less: -------------------------------------------------------------------------------- 1 | .warpper { 2 | padding: 10px; 3 | } 4 | 5 | .header { 6 | padding: 15px 25px 25px; 7 | font-size: 24px; 8 | color: #c1c1c1; 9 | // font-size: 18px; 10 | font-weight: 700; 11 | display: flex; 12 | .title { 13 | flex: 1; 14 | } 15 | .tag { 16 | font-size: 12px; 17 | line-height: 25px; 18 | font-weight: normal; 19 | color: #868686; 20 | margin: 0 0 0 10px; 21 | span { 22 | display: inline-block; 23 | padding: 0 3px 0 3px; 24 | border-radius: 2px; 25 | height: 18px; 26 | line-height: 18px; 27 | margin: 0 2px; 28 | cursor: pointer; 29 | transition: background-color .3s; 30 | &:hover, &:global(.active) { 31 | background-color: #868686; 32 | color: #fff; 33 | } 34 | } 35 | } 36 | } 37 | 38 | .noFind { 39 | min-height: 64px; 40 | align-items: center; 41 | justify-content: center; 42 | display: flex; 43 | color: #969696; 44 | } 45 | 46 | .lists { 47 | display: flex; 48 | flex-direction: row; 49 | flex-wrap: wrap; 50 | padding: 0 10px; 51 | li { 52 | display: flex; 53 | flex-direction: column; 54 | flex-grow: 1; 55 | margin: 0 5px 10px 5px; 56 | min-width: 160px; 57 | border-radius: 5px; 58 | background-color: var(--color-active); 59 | box-shadow: 0 1px 2px rgba(0,0,0,.07); 60 | transition: all .3s; 61 | &:hover { 62 | box-shadow: 0 0px 5px rgba(0, 0, 0, 0.6); 63 | .star:not(:global(.active)) svg{ 64 | animation-duration: 1s; 65 | animation-fill-mode: both; 66 | animation: twinkling .6s infinite ease-in-out; 67 | } 68 | } 69 | a { 70 | text-decoration: none; 71 | } 72 | } 73 | } 74 | @keyframes twinkling{ 75 | 0%{ 76 | fill: #D64B24; 77 | } 78 | 50%{ 79 | fill: #e0e0e0; 80 | } 81 | 100%{ 82 | fill: #D64B24; 83 | } 84 | } 85 | 86 | .itemHeader { 87 | flex: 1; 88 | } 89 | 90 | .logo { 91 | display: flex; 92 | overflow: hidden; 93 | text-overflow: ellipsis; 94 | white-space: nowrap; 95 | align-items: center; 96 | padding: 5px; 97 | h4 { 98 | padding: 0 10px 0 5px; 99 | margin: 0; 100 | line-height: 16px; 101 | height: 16px; 102 | vertical-align: middle; 103 | flex: 1; 104 | } 105 | img { 106 | height: 23px; 107 | min-width: 23px; 108 | min-height: 23px; 109 | vertical-align: middle; 110 | border-radius: 3px; 111 | padding: 3px; 112 | } 113 | } 114 | 115 | .details { 116 | padding: 0px 10px 5px 10px; 117 | max-width: 190px; 118 | color: #969696; 119 | } 120 | 121 | .bottomBar { 122 | padding: 0 10px; 123 | margin: 0 0 5px 0; 124 | display: flex; 125 | svg { 126 | fill: #dcdbdb; 127 | fill: #e0e0e0; 128 | transition: all .3s; 129 | margin-right: 3px; 130 | &:hover { 131 | fill: #333; 132 | } 133 | } 134 | img, svg { 135 | width: 12px; 136 | height: 12px; 137 | } 138 | .urls { 139 | flex: 1; 140 | } 141 | .star { 142 | display: flex; 143 | align-items: center; 144 | &:global(.active) { 145 | svg { 146 | fill: #D64B24; 147 | &:hover { 148 | fill: #D64B24; 149 | } 150 | } 151 | } 152 | svg { 153 | cursor: pointer; 154 | &:hover { 155 | fill: #fbb601; 156 | } 157 | } 158 | } 159 | } 160 | 161 | .zhHans { 162 | circle, path { 163 | color: #606467; 164 | } 165 | path { 166 | fill: #fff; 167 | } 168 | &:hover { 169 | circle { 170 | fill: #D64B24; 171 | } 172 | path { 173 | fill: #FBDC00; 174 | } 175 | } 176 | } 177 | 178 | .search { 179 | outline: 0; 180 | border: 0; 181 | background-color: transparent; 182 | border-bottom: 1px solid #afafaf; 183 | margin-right: 10px; 184 | padding-bottom: 3px; 185 | max-width: 80px; 186 | text-align: center; 187 | &::placeholder { 188 | color: #afafaf; 189 | text-align: center; 190 | } 191 | } -------------------------------------------------------------------------------- /src/pages/Github.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import cheerio from 'cheerio'; 3 | import { fetchInterval, fetchTimely } from '../utils'; 4 | import Footer from '../component/Footer'; 5 | import Select from '../component/Select'; 6 | import Loading from '../component/Loading'; 7 | import styles from './Github.module.less'; 8 | import optionLang from '../source/trending.json'; 9 | 10 | const githublist = localStorage.getItem('github-list'); 11 | 12 | const starSVG = ( 13 | 14 | 15 | 16 | ); 17 | 18 | export default class Github extends Component { 19 | static typeName = 'trending'; 20 | constructor(props) { 21 | super(props); 22 | this.state = { 23 | loading: false, 24 | content: githublist ? JSON.parse(githublist) : null, 25 | option: [ 26 | { 27 | label: '今天', 28 | value: '', 29 | }, { 30 | label: '本周', 31 | value: 'weekly', 32 | }, { 33 | label: '本月', 34 | value: 'monthly', 35 | }, 36 | ], 37 | optionLang, 38 | suggest: ['go', 'html', 'javascript', 'python', 'swift'], 39 | since: props.conf.githubSince, 40 | lang: props.conf.githubLang, 41 | }; 42 | } 43 | componentWillUnmount() { 44 | this.mounted = false; 45 | } 46 | componentDidMount() { 47 | this.mounted = true; 48 | this.getTrending(); 49 | } 50 | getURL() { 51 | const { since, lang } = this.state; 52 | let url = 'https://github.com/trending'; 53 | if (lang) url = `${url}/${lang}`; 54 | if (since) url = `${url}?since=${since}`; 55 | return url; 56 | } 57 | getTrending(type) { 58 | let localContent = localStorage.getItem('github-list'); 59 | if (!localContent) type = 'select'; // 判断是否直接选择 60 | else { 61 | localContent = JSON.parse(localContent); 62 | } 63 | this.setState({ loading: true }); 64 | const getDate = type === 'select' ? fetchTimely(this.getURL()) : fetchInterval(this.getURL(), 3, 'github-trending'); 65 | getDate.then((response) => { 66 | response.replace(/]*>([\s\S]*?)<\/body>/gi, (node, body) => { 67 | response = body; 68 | return node; 69 | }); 70 | response = response.replace(/]+\bhref="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, (node, url, text) => { 71 | if (/^\//.test(url)) { 72 | node = `${text}`; 73 | } 74 | return node; 75 | }); 76 | const resultData = []; 77 | const $ = cheerio.load(response); 78 | $('.Box-row').each((idx, item) => { 79 | // 不需要头像,避免被和谐 80 | /* eslint-disable */ 81 | const fullName = $(item).find('h1 a').text().replace(/(\n|\s)/g, ''); 82 | const href = $(item).find('h1 a').attr('href').replace(/(\n|\s)/g, ''); 83 | const language = $(item).find('span[itemprop=programmingLanguage]').text().replace(/(\n|\s)/g, ''); 84 | const languageColor = $(item).find('span.repo-language-color'); 85 | const todayStar = $(item).find('span.float-sm-right').text().replace(/(\n|,)/g, '').trim(); 86 | const description = $(item).find('p.text-gray').text().replace(/(\n)/g, '').trim(); 87 | /* eslint-enable */ 88 | let color = ''; 89 | if (language && languageColor && languageColor.css) { 90 | color = languageColor.css('background-color'); 91 | } 92 | let stargazersCount = ''; 93 | let node = $(item).find('svg[aria-label="star"].octicon.octicon-star'); 94 | if (node && node[0] && node[0].next) { 95 | stargazersCount = node[0].next.data.replace(/(\n|\s|,)/g, ''); 96 | } 97 | 98 | let forked = '-'; 99 | node = $(item).find('svg[aria-label="repo-forked"].octicon.octicon-repo-forked'); 100 | if (node) { 101 | forked = node[0].next.data.replace(/(\n|\s|,)/g, ''); 102 | } 103 | 104 | resultData.push({ full_name: fullName, language, color, description, forked, stargazers_count: parseInt(stargazersCount, 10), todayStar, html_url: href, rank: idx + 1 }); 105 | }); 106 | if (!resultData) return; 107 | localStorage.setItem('github-list', JSON.stringify(resultData)); 108 | if (!this.mounted) return; 109 | this.setState({ 110 | loading: false, 111 | content: resultData, 112 | }); 113 | }).catch(() => { 114 | this.setState({ loading: false }); 115 | if (!this.mounted) return; 116 | this.setState({ 117 | content: this.state.content || '请求错误,请检查网路,或者重新刷新请求数据!', 118 | }); 119 | }); 120 | } 121 | onSelect(type, item) { 122 | const { storage, conf } = this.props; 123 | this.setState({ loading: false }); 124 | if (!type) return; 125 | this.setState({ 126 | [`${type}`]: item.value, 127 | }, () => { 128 | conf[type === 'since' ? 'githubSince' : 'githubLang'] = item.value; 129 | storage.set({ conf }, () => { 130 | this.getTrending('select'); 131 | }); 132 | }); 133 | } 134 | render() { 135 | const { content } = this.state; 136 | return ( 137 |
138 |
139 | Github Trending 140 |
141 | 142 | 151 |
152 |
153 |
154 | {!content ? ( 155 |
Loading...
156 | ) : ( 157 |
    158 | {content.map((item, idx) => { 159 | return ( 160 |
  • 161 |

    162 | {item.full_name} 163 |

    164 |
    165 | {item.description} 166 |
    167 |
    168 | {item.language} 169 | 170 | {starSVG} 171 | {item.stargazers_count} 172 | 173 | 174 | 175 | 176 | 177 | {item.forked} 178 | 179 | {starSVG}{item.todayStar} 180 |
    181 |
  • 182 | ); 183 | })} 184 |
185 | )} 186 |
187 | {githublist &&
已显示全部内容
} 188 |
189 | ); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/pages/Github.module.less: -------------------------------------------------------------------------------- 1 | .warpper { 2 | padding: 10px; 3 | .header { 4 | padding: 15px 25px 25px; 5 | color: #c1c1c1; 6 | display: flex; 7 | } 8 | .title { 9 | margin: 0; 10 | font-size: 24px; 11 | color: #c1c1c1; 12 | font-size: 18px; 13 | font-weight: 700; 14 | flex: 1; 15 | a { 16 | color: #c1c1c1; 17 | text-decoration: none; 18 | &:hover { 19 | color: #0080d0; 20 | } 21 | } 22 | } 23 | .select { 24 | font-size: 12px; 25 | line-height: 25px; 26 | font-weight: normal; 27 | color: #868686; 28 | margin: 0 0 0 10px; 29 | } 30 | } 31 | 32 | .list { 33 | ul { 34 | box-sizing: border-box; 35 | display: flex; 36 | flex-direction: row; 37 | flex-wrap: wrap; 38 | } 39 | a { 40 | text-decoration: none; 41 | } 42 | li, h3 { 43 | margin: 0; 44 | padding: 0; 45 | } 46 | li { 47 | list-style: none; 48 | min-width: 0; 49 | box-sizing: border-box; 50 | background-color: #fff; 51 | border-radius: 3px; 52 | transition: all .3s; 53 | margin-bottom: 10px; 54 | padding: 10px; 55 | flex: calc(~"50% - 10px"); 56 | margin-left: 5px; 57 | margin-right: 5px; 58 | display: inline-block; 59 | &:hover { 60 | background-color: #f9f9f9; 61 | } 62 | h3 a { 63 | font-size: 14px; 64 | line-height: 18px; 65 | font-weight: bold; 66 | color: #0366d6; 67 | display: block; 68 | transition: color .3s; 69 | &:hover { 70 | text-decoration: underline; 71 | } 72 | } 73 | .description { 74 | padding: 8px 0; 75 | line-height: 14px; 76 | min-height: 28px; 77 | } 78 | .star, .language, .forked, .todayStar { 79 | display: inline-block; 80 | margin-right: 8px!important; 81 | svg { 82 | vertical-align: middle; 83 | margin-right: 5px; 84 | height: 14px; 85 | margin-top: -3px; 86 | } 87 | } 88 | .language { 89 | span { 90 | border-radius: 50%; 91 | display: inline-block; 92 | height: 12px; 93 | position: relative; 94 | top: 1px; 95 | width: 12px; 96 | margin-right: 5px; 97 | } 98 | } 99 | } 100 | } 101 | 102 | 103 | 104 | @media screen and (max-width: 700px) { 105 | .list { 106 | li { 107 | width: 100%; 108 | margin-left: 0; 109 | margin-right: 0; 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/pages/History.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import classNames from 'classnames'; 3 | import Footer from '../component/Footer'; 4 | import styles from './History.module.less'; 5 | 6 | function isToday(str) { 7 | const d = new Date(str.replace(/-/g, '/')); 8 | const todaysDate = new Date(); 9 | if (d.setHours(0, 0, 0, 0) === todaysDate.setHours(0, 0, 0, 0)) { 10 | return true; 11 | } 12 | return false; 13 | } 14 | 15 | const time = (str) => { 16 | const date = str ? new Date(str) : new Date(); 17 | const hours = date.getHours(); 18 | const minutes = date.getMinutes(); 19 | // const seconds = date.getSeconds(); 20 | const year = date.getFullYear(); 21 | const month = date.getMonth() + 1; 22 | const day = date.getDate(); 23 | const today = isToday(`${year}/${month}/${day}`); 24 | return ( 25 |
26 | {!today && {`${month}/${day} `}} 27 | {`${hours < 10 ? `0${hours}` : hours}`} 28 | {`${minutes < 10 ? `0${minutes}` : minutes}`} 29 |
30 | ); 31 | }; 32 | 33 | export default class History extends PureComponent { 34 | static typeName = 'history' 35 | constructor(props) { 36 | super(props); 37 | this.state = { 38 | list: [], 39 | tab: props.conf.historyTabType, 40 | // tab: 'today', 41 | }; 42 | this.getHistory(); 43 | } 44 | getHistory() { 45 | const { tab } = this.state; 46 | let microsecondsPerWeek = 1000 * 60 * 60 * 24 * 7; 47 | if (tab === 'today') { 48 | microsecondsPerWeek = (new Date()).getTime() - (new Date('2018/02/13 00:00:00')).getTime(); 49 | } 50 | if (tab === 'week') { 51 | microsecondsPerWeek = 1000 * 60 * 60 * 24 * 7; 52 | } 53 | if (tab === 'all') { 54 | microsecondsPerWeek = 1000 * 60 * 60 * 24 * 7; 55 | } 56 | const oneWeekAgo = (new Date()).getTime() - microsecondsPerWeek; 57 | // eslint-disable-next-line 58 | chrome.history.search({ 59 | text: '', 60 | startTime: oneWeekAgo, 61 | }, (historyItems) => { 62 | this.setState({ 63 | list: historyItems, 64 | }); 65 | }); 66 | } 67 | onClickClean() { 68 | // eslint-disable-next-line 69 | chrome.history.deleteAll(() => { 70 | this.setState({ 71 | list: [], 72 | }); 73 | }); 74 | } 75 | onClickGetData(type) { 76 | const { storage, conf } = this.props; 77 | conf.historyTabType = type; 78 | storage.set({ conf }); 79 | 80 | this.setState({ tab: type }, () => { 81 | this.getHistory(); 82 | }); 83 | } 84 | render() { 85 | const { tab } = this.state; 86 | return ( 87 |
88 |
89 | 历史记录 90 |
91 | 今天 92 | 一周 93 | 全部 94 | 清空所有 95 |
96 |
97 |
    98 | {this.state.list.length === 0 &&
  • 没有历史记录!
  • } 99 | {this.state.list.map((item, idx) => { 100 | // console.log(new Date(item.lastVisitTime)); 101 | // const date = new Date(item.lastVisitTime) 102 | return ( 103 |
  • 104 |
    105 |
    {time(item.lastVisitTime)}
    106 | {item.title || item.url} 107 |
  • 108 | ); 109 | })} 110 |
111 |
已显示全部内容
112 |
113 | ); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/pages/History.module.less: -------------------------------------------------------------------------------- 1 | .warpper { 2 | padding: 10px; 3 | } 4 | 5 | .header { 6 | padding: 15px 25px 25px; 7 | font-size: 24px; 8 | font-size: 18px; 9 | font-weight: 700; 10 | display: flex; 11 | .title { 12 | flex: 1; 13 | } 14 | .setting { 15 | font-size: 12px; 16 | line-height: 25px; 17 | font-weight: normal; 18 | color: #868686; 19 | margin: 0 0 0 10px; 20 | span { 21 | display: inline-block; 22 | color: #868686; 23 | padding: 1px 4px 0 4px; 24 | border-radius: 2px; 25 | line-height: 18px; 26 | margin: 0 2px; 27 | transition: background-color 0.3s; 28 | cursor: pointer; 29 | } 30 | span:hover { 31 | background-color: #868686; 32 | color: #fff; 33 | } 34 | :global { 35 | .active { 36 | background-color: #868686; 37 | color: #fff; 38 | } 39 | } 40 | } 41 | } 42 | 43 | .list { 44 | line-height: 32px; 45 | padding: 5px 0; 46 | margin: 0 15px; 47 | background-color: var(--color-active); 48 | a { 49 | display: block; 50 | padding: 0 10px; 51 | transition: all .3s; 52 | text-overflow: ellipsis; 53 | white-space: nowrap; 54 | overflow: hidden; 55 | position: relative; 56 | flex: 1; 57 | &:hover { 58 | color: #0e693c; 59 | background-color: var(--color-active); 60 | } 61 | } 62 | li { 63 | line-height: 32px; 64 | min-height: 32px; 65 | padding: 0 10px; 66 | display: flex; 67 | flex-direction: row; 68 | align-items: center; 69 | } 70 | } 71 | 72 | .favicon { 73 | display: block; 74 | width: 16px; 75 | height: 16px; 76 | margin-right: 10px; 77 | } 78 | 79 | .hours { 80 | color: #a5a5a5; 81 | } 82 | .minutes { 83 | color: #a5a5a5; 84 | &::before { 85 | content: ':'; 86 | display: inline-block; 87 | position: relative; 88 | top: -1px; 89 | padding: 0 1px; 90 | } 91 | } -------------------------------------------------------------------------------- /src/pages/Linux/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import command from 'linux-command'; 3 | import classNames from 'classnames'; 4 | import styles from './index.module.less'; 5 | import Search from '../../component/Search'; 6 | import Footer from '../../component/Footer'; 7 | import logo from '../../assets/linux-logo.svg'; 8 | 9 | export default class Linux extends Component { 10 | static typeName = 'linux' 11 | constructor(props) { 12 | super(props); 13 | this.state = { 14 | query: '', 15 | active: null, 16 | }; 17 | } 18 | onChange(e) { 19 | if (!e.target) return; 20 | this.setState({ query: e.target.value, active: !e.target.value ? null : 0 }); 21 | } 22 | openURL() { 23 | const { query, active } = this.state; 24 | const ContentData = this.filterItems(); 25 | let queryStr = query; 26 | if ((active || active === 0) && ContentData[active]) { 27 | queryStr = ContentData[active].n; 28 | } 29 | if ((active || (active === 0 && query === queryStr) || query === queryStr) && queryStr && query) { 30 | window.open(`https://jaywcjlove.github.io/linux-command/c/${queryStr}.html`); 31 | } else { 32 | window.open(`https://jaywcjlove.github.io/linux-command/list.html#!kw=${query}`); 33 | } 34 | } 35 | onSearch() { 36 | this.openURL(); 37 | } 38 | onKeyUp(e) { 39 | const key = e.keyCode || e.which || e.charCode; 40 | const { active } = this.state; 41 | if (key === 13) { // 摁Enter 42 | this.openURL(); 43 | } 44 | const indexItem = active || active === 0 ? active : -1; 45 | if (key === 40 && indexItem + 1 < this.filterItems().length) { // 摁下 46 | this.setState({ 47 | active: indexItem + 1, 48 | }); 49 | } 50 | if (key === 38 && indexItem > 0) { // 摁上 51 | this.setState({ 52 | active: indexItem - 1, 53 | }); 54 | } 55 | } 56 | filterItems() { 57 | const { query } = this.state; 58 | const ContentData = Object.keys(command).map(item => command[item]); 59 | return ContentData.filter((item) => { 60 | return item.n.indexOf(query) > -1 || item.d.indexOf(query) > -1; 61 | }); 62 | } 63 | render() { 64 | const { query, active } = this.state; 65 | const ContentData = this.filterItems(); 66 | const Content = ContentData.map((item, idx) => { 67 | let name = item.n; 68 | let des = item.d; 69 | if (query && (name.indexOf(query) > -1 || des.indexOf(query) > -1)) { 70 | const reg = new RegExp(`(${query})`, 'ig'); 71 | name = name.replace(reg, `$1`); // eslint-disable-line 72 | des = des.replace(reg, `$1`); // eslint-disable-line 73 | } 74 | if (idx > 10) return null; 75 | return ( 76 |
77 | 82 | 编辑 83 |
84 | ); 85 | }); 86 | return ( 87 |
88 |
89 | linux-command logo 90 |
91 | 92 |
93 | {Object.keys(command).length > 0 && ( 94 |
95 | 共 {Object.keys(command).length} 个Linux命令 96 | 添加命令 97 |
98 | )} 99 | {Content.length === 0 ?
没有搜索到内容
: Content} 100 |
101 | 102 |
103 | ); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/pages/Linux/index.module.less: -------------------------------------------------------------------------------- 1 | 2 | .warpper { 3 | max-width: 517px; 4 | margin: 0 auto 0; 5 | } 6 | 7 | .header { 8 | text-align: center; 9 | padding: 60px 0 40px 0; 10 | } 11 | 12 | .list { 13 | padding: 5px 5px 15px 5px; 14 | margin-top: 20px; 15 | border-radius: 5px; 16 | font-size: 14px; 17 | line-height: 32px; 18 | background-color: var(--color-active); 19 | min-height: 100px; 20 | a { 21 | text-decoration: none; 22 | :global(.kw) { 23 | color: red; 24 | font-style: initial; 25 | } 26 | } 27 | .info { 28 | // color: #555; 29 | font-size: 14px; 30 | text-align: center; 31 | } 32 | .infoTotal { 33 | // color:#b3b3b3; 34 | display: flex; 35 | padding: 0 10px; 36 | span { 37 | flex: 1; 38 | } 39 | &:hover a{ 40 | color: #0366d6; 41 | } 42 | a { 43 | // color: #b3b3b3; 44 | &:hover { 45 | color: #034ad6; 46 | } 47 | } 48 | } 49 | .item { 50 | display: flex; 51 | line-height: 14px; 52 | padding: 6px 10px; 53 | &:global(.active) { 54 | background-color: var(--color-active); 55 | } 56 | &:hover{ 57 | background-color: var(--color-active); 58 | } 59 | .title { 60 | flex: 1; 61 | display: block; 62 | text-overflow: ellipsis; 63 | white-space: nowrap; 64 | overflow: hidden; 65 | } 66 | .edit { 67 | display: none; 68 | } 69 | &:hover .edit{ 70 | display: inline-block; 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /src/pages/Navigation/Edit.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import classNames from 'classnames'; 3 | import styles from './Edit.module.less'; 4 | import websiteIcon from '../../assets/website.svg'; 5 | import document from '../../dev-site/src/document.json'; 6 | import website from '../../source/website.json'; 7 | import addIcon from '../../assets/add-icon.png'; 8 | 9 | const blankData = { 10 | label: '', 11 | value: '', 12 | icon: addIcon, 13 | }; 14 | 15 | export default class Edit extends Component { 16 | constructor(props) { 17 | super(props); 18 | this.state = { 19 | websiteSource: website, 20 | active: '', 21 | edit: { ...blankData }, 22 | }; 23 | } 24 | componentDidMount() { 25 | const { websiteSource } = this.state; 26 | this.setListData(); 27 | this.onHideEdit(); 28 | const documentSource = { 29 | label: '开发文档', 30 | value: 'document', 31 | children: [], 32 | }; 33 | documentSource.children = document.map((item) => { 34 | return { 35 | label: item.title, 36 | value: item.website, 37 | icon: item.logo || websiteIcon, 38 | urls: item.urls || '', 39 | _website: item.website, 40 | type: 'doc', 41 | }; 42 | }); 43 | const doc = websiteSource.filter(item => item.value === 'document'); 44 | if (doc && doc.length === 0) { 45 | websiteSource.push(documentSource); 46 | this.setState({ websiteSource }); 47 | } 48 | } 49 | setListData(type) { 50 | let list = []; 51 | for (let i = 0; i < website.length; i += 1) { 52 | if (type) { 53 | if (website[i].value === type) { 54 | list = website[i].children; 55 | break; 56 | } 57 | } else if (website[i].children && website[i].children.length > 0) { 58 | list = website[i].children; 59 | break; 60 | } 61 | } 62 | this.setState({ list }); 63 | } 64 | onClickTab(type) { 65 | this.setListData(type); 66 | this.setState({ active: type, edit: { ...blankData } }); 67 | } 68 | onClickAdd(edit) { 69 | const { onClickAdd } = this.props; 70 | if (!edit.value) return; 71 | this.onHideEdit(); 72 | onClickAdd && onClickAdd(edit); 73 | } 74 | handleAddNav(edit) { 75 | const { onClickGrid } = this.props; 76 | this.setState({ edit }, () => { 77 | onClickGrid && onClickGrid(edit); 78 | }); 79 | } 80 | onShowEdit() { 81 | this.warpper.style.marginBottom = '0px'; 82 | } 83 | onHideEdit() { 84 | this.warpper.style.marginBottom = `-${this.warpper.clientHeight}px`; 85 | } 86 | onChangeEdit(e) { 87 | const { websiteSource } = this.state; 88 | const value = e.target.value; 89 | const wb = {}; 90 | wb.icon = ''; 91 | wb.value = value; 92 | wb.label = value; 93 | websiteSource.forEach((item) => { 94 | item.children.forEach((itemChild) => { 95 | if (value.indexOf(itemChild.value) > -1) { 96 | wb.icon = itemChild.icon; 97 | // wb.label = itemChild.label; 98 | } 99 | }); 100 | }); 101 | this.setState({ edit: wb }); 102 | } 103 | onChangeTitleEdit(e) { 104 | const { edit } = this.state; 105 | const value = e.target.value; 106 | edit.label = value; 107 | this.setState({ edit }); 108 | } 109 | handleClickEditBtn(url) { 110 | const { edit } = this.state; 111 | edit.value = url; 112 | this.setState({ edit }); 113 | } 114 | renderEditItemURL(edit) { 115 | const { urls, label } = edit; 116 | const items = []; 117 | for (const a in urls) { 118 | if (Object.prototype.hasOwnProperty.call(urls, a)) { 119 | let title = a; 120 | if (a === 'git') title = '开源仓库'; 121 | if (a === 'cn') title = '中文文档'; 122 | items.push({title}); 123 | } 124 | } 125 | if (items.length > 1) items.push(官网); 126 | return items; 127 | } 128 | render() { 129 | const { active, edit, websiteSource } = this.state; 130 | return ( 131 |
this.warpper = node}> 132 |
133 | {edit.label} e.target.src = websiteIcon} src={edit.icon} /> 134 | 135 | 136 | {edit.label} 137 | 138 | 139 | 140 | {active === 'document' && edit.urls &&
{this.renderEditItemURL(edit)}
} 141 |
142 |
143 |
144 | {websiteSource.map((item, idx) => { 145 | let isActive = item.value === active; 146 | if (active === '' && idx === 0) isActive = true; 147 | return ( 148 | 149 | {item.label} 150 | 151 | ); 152 | })} 153 |
154 | {websiteSource.map((item, idx) => { 155 | return ( 156 |
163 | {item.children.map((_item, _idx) => ( 164 | 165 | {_item.label} e.target.src = websiteIcon} src={_item.icon} /> 166 |

{_item.label}

167 |
168 | ))} 169 |
170 | ); 171 | })} 172 |
173 |
174 |
175 | ); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/pages/Navigation/Edit.module.less: -------------------------------------------------------------------------------- 1 | .navEdit { 2 | min-height: 160px; 3 | margin-bottom: -405px; 4 | background-color: #333; 5 | width: 100%; 6 | bottom: 0; 7 | position: absolute; 8 | z-index: 1; 9 | transition: all .3s; 10 | .edit { 11 | display: flex; 12 | position: relative; 13 | margin: 0 50px; 14 | padding: 40px 0 0 0; 15 | box-sizing: border-box; 16 | &:global(.doc) { 17 | img { 18 | background-color: #fff; 19 | padding: 5px; 20 | } 21 | } 22 | img { 23 | width: 31px; 24 | height: 31px; 25 | align-self: center; 26 | border-radius: 3px; 27 | background-color: #ffffff1c; 28 | } 29 | .title { 30 | display: block; 31 | margin-right: 5px; 32 | margin-left: 5px; 33 | align-self: center; 34 | position: relative; 35 | width: 100px; 36 | overflow: hidden; 37 | white-space: nowrap; 38 | text-overflow: ellipsis; 39 | color: rgba(255,255,255,.8); 40 | min-height: 32px; 41 | line-height: 32px; 42 | .titleInput { 43 | color: rgba(255, 255, 255, 0.8); 44 | width: 100%; 45 | height: 32px; 46 | padding: 0 5px; 47 | position: absolute; 48 | &:hover { 49 | background-color: #505050; 50 | } 51 | } 52 | } 53 | .titleInput, .url { 54 | border-radius: 2px; 55 | outline: 0; 56 | border: 0; 57 | background-color: #414141; 58 | transition: all .3s; 59 | } 60 | .url { 61 | padding: 0 10px; 62 | max-width: 400px; 63 | width: 100%; 64 | color: rgba(255,255,255,.8); 65 | &:hover { 66 | background-color: #505050; 67 | } 68 | } 69 | .save { 70 | margin-left: 10px; 71 | border-radius: 2px; 72 | width: 74px; 73 | border: 0; 74 | outline: none; 75 | cursor: pointer; 76 | transition: all .3s; 77 | white-space: nowrap; 78 | &:hover { 79 | background-color: #cccccc; 80 | } 81 | &:active { 82 | background-color: #076a39; 83 | color: #fff; 84 | } 85 | } 86 | .otherUrl { 87 | display: flex; 88 | flex-flow: row wrap; 89 | align-content: flex-start; 90 | flex-wrap: nowrap; 91 | span { 92 | margin: 0 0 0 5px; 93 | cursor: pointer; 94 | padding: 0 11px; 95 | border-radius: 2px; 96 | background-color: #d4f5db; 97 | line-height: 32px; 98 | transition: all .3s; 99 | overflow: hidden; 100 | text-overflow: ellipsis; 101 | white-space: nowrap; 102 | &:hover { 103 | background-color: #7eca8e; 104 | } 105 | &:active { 106 | background-color: #076a39; 107 | color: #fff; 108 | } 109 | } 110 | } 111 | } 112 | .editTabList { 113 | padding: 20px 0 0 0; 114 | color: #fff; 115 | margin: 0 50px; 116 | .category { 117 | display: flex; 118 | font-size: 14px; 119 | font-weight: 200; 120 | color: rgba(255,255,255,.4); 121 | border-bottom: 1px solid rgba(255,255,255,.05); 122 | padding-bottom: 12px; 123 | :global(.active) { 124 | color: #fff; 125 | position: relative; 126 | &::before { 127 | position: absolute; 128 | bottom: -12px; 129 | border-bottom: 1px solid rgba(24,177,255,.8); 130 | width: 100%; 131 | content: ''; 132 | } 133 | } 134 | span { 135 | margin-right: 10px; 136 | cursor: pointer; 137 | } 138 | } 139 | .list { 140 | display: flex; 141 | flex-flow: row wrap; 142 | align-content: flex-start; 143 | padding: 30px 0; 144 | min-height: 280px; 145 | max-height: 280px; 146 | display: none; 147 | overflow: auto; 148 | &:global(.show) { 149 | display: flex; 150 | } 151 | &:global(.doc) { 152 | img { 153 | background-color: #fff; 154 | padding: 10px; 155 | } 156 | } 157 | span { 158 | display: inline-block; 159 | margin: 0 10px; 160 | user-select: none; 161 | cursor: pointer; 162 | &:hover > img { 163 | filter:brightness(0.96); 164 | } 165 | &:active > img { 166 | filter:brightness(0.66); 167 | } 168 | } 169 | img { 170 | border-radius: 3px; 171 | display: block; 172 | background-color: #ffffff36; 173 | width: 62px; 174 | height: 62px; 175 | filter: brightness(0.99); 176 | } 177 | p { 178 | width: 62px; 179 | font-size: 12px; 180 | text-align: center; 181 | white-space: nowrap; 182 | overflow: hidden; 183 | text-overflow: ellipsis; 184 | } 185 | } 186 | } 187 | .closeBtn { 188 | height: 23px; 189 | width: 23px; 190 | bottom: 20px; 191 | right: 20px; 192 | cursor: pointer; 193 | position: absolute; 194 | &::before, &::after { 195 | content: ''; 196 | display: block; 197 | background-color: #fff; 198 | position: absolute; 199 | border-radius: 3px; 200 | height: 2px; 201 | width: 100%; 202 | } 203 | &::before { 204 | transform: rotate(45deg); 205 | } 206 | &::after { 207 | transform: rotate(-45deg); 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/pages/Navigation/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import classNames from 'classnames'; 3 | import styles from './index.module.less'; 4 | import addIcon from '../../assets/add-icon.png'; 5 | import websiteIcon from '../../assets/website.svg'; 6 | import website from '../../source/website.json'; 7 | import Edit from './Edit'; 8 | // import Contextmenu from "../../component/Contextmenu"; 9 | 10 | export default class Navigation extends Component { 11 | static typeName = 'navigation' 12 | constructor(props) { 13 | super(props); 14 | this.state = { 15 | navContent: props.dbs.nav, 16 | optionDown: false, 17 | styleItem: {}, 18 | // navContent: [ 19 | // { 20 | // label: '500px', 21 | // value: 'https://500px.com/', 22 | // icon: 'https://jaywcjlove.github.io/logo/500px.png', 23 | // }, 24 | // ], 25 | }; 26 | this.handleResize = this.handleResize.bind(this); 27 | this.handleClickOption = this.handleClickOption.bind(this); 28 | this.handleClickOptionUp = this.handleClickOptionUp.bind(this); 29 | } 30 | componentWillUnmount() { 31 | document.removeEventListener('keydown', this.handleClickOption, true); 32 | document.removeEventListener('keyup', this.handleClickOptionUp, true); 33 | } 34 | componentDidMount() { 35 | window.addEventListener('resize', this.handleResize, true); 36 | document.addEventListener('keydown', this.handleClickOption, true); 37 | document.addEventListener('keyup', this.handleClickOptionUp, true); 38 | const { navContent } = this.state; 39 | const { storage, dbs } = this.props; 40 | if (navContent.length === 0) { 41 | for (let i = 0; i < website.length; i += 1) { 42 | if (website[i].children && website[i].children[0]) { 43 | navContent.push(website[i].children[0]); 44 | dbs.nav = navContent; 45 | storage.set({ dbs }); 46 | this.setState({ navContent }); 47 | } 48 | } 49 | } 50 | } 51 | handleClickOption(e) { 52 | const key = e.keyCode || e.which || e.charCode; 53 | if (e.key === 'Alt' || key === 18) { 54 | this.setState({ optionDown: true }); 55 | } 56 | } 57 | handleClickOptionUp(e) { 58 | const key = e.keyCode || e.which || e.charCode; 59 | if (e.key === 'Alt' || key === 18) { 60 | this.setState({ optionDown: false }); 61 | } 62 | } 63 | handleResize() { 64 | if (this.navContent) { 65 | this.resizeContent(this.navContent); 66 | } 67 | } 68 | resizeContent(node) { 69 | if (!node) return; 70 | this.navContent = node; 71 | let width = document.body.clientWidth; 72 | width -= 100; 73 | if (width > 1024) width = 1024; 74 | if (width < 660) width = 660; 75 | const subWidth = width / 6; 76 | let paddingHorizontal = (subWidth - 90) / 2; 77 | if (paddingHorizontal < 20) paddingHorizontal = 20; 78 | node.style.maxHeight = '504px'; 79 | node.style.width = `${width}px`; 80 | const child = node.children; 81 | for (let i = 0; i < child.length; i += 1) { 82 | if (child[i]) { 83 | child[i].style.width = `${subWidth}px`; 84 | child[i].style.paddingLeft = `${paddingHorizontal}px`; 85 | child[i].style.paddingRight = `${paddingHorizontal}px`; 86 | const img = child[i].getElementsByTagName('img'); 87 | if (img && img.length > 0) { 88 | img[0].style.height = `${img[0].clientWidth}px`; 89 | } 90 | } 91 | } 92 | } 93 | onClickAdd(item) { 94 | const { navContent } = this.state; 95 | const { storage, dbs } = this.props; 96 | const itemfilter = navContent.filter(editItem => editItem.value === item.value); 97 | if (itemfilter.length > 0) return; 98 | 99 | navContent.push(item); 100 | dbs.nav = navContent; 101 | storage.set({ dbs }); 102 | this.setState({ navContent }); 103 | } 104 | onShowEdit = () => { 105 | this.edit.onShowEdit(); 106 | } 107 | onKeyDownOption(item) { 108 | const { storage, dbs } = this.props; 109 | const { navContent } = this.state; 110 | const itemfilter = navContent.filter(editItem => editItem.value !== item.value); 111 | dbs.nav = itemfilter; 112 | storage.set({ dbs }); 113 | this.setState({ navContent: itemfilter }); 114 | } 115 | onContextMenu(e) { 116 | e.preventDefault(); 117 | const optionDown = !this.state.optionDown; 118 | this.setState({ optionDown }); 119 | return false; 120 | } 121 | onClickContextMenu(e) { 122 | const { optionDown } = this.state; 123 | if (optionDown) { 124 | e.preventDefault(); 125 | } 126 | this.setState({ optionDown: false }); 127 | } 128 | onDragOver(e) { 129 | this.DragOverElm = e.target; 130 | } 131 | onDragStart(e) { 132 | this.parentElm = e.target.parentNode; 133 | e.target.classList.add('is-drod'); 134 | } 135 | onDragEnd(e) { 136 | const { navContent } = this.state; 137 | const { storage, dbs } = this.props; 138 | const child = this.parentElm.children; 139 | e.target.classList.remove('is-drod'); 140 | let overIndex = null; 141 | let currentIndex = null; 142 | for (let i = 0; i < child.length; i += 1) { 143 | if (child[i] === this.DragOverElm) overIndex = i; 144 | if (child[i] === e.target) currentIndex = i; 145 | } 146 | if (currentIndex > -1 && overIndex > -1) { 147 | const curData = navContent[currentIndex]; 148 | if (currentIndex > overIndex) { 149 | // this.parentElm.insertBefore(e.target, this.DragOverElm); 150 | navContent.splice(currentIndex, 1); 151 | navContent.splice(overIndex, 0, curData); 152 | } 153 | if (currentIndex < overIndex) { 154 | // this.parentElm.insertBefore(e.target, this.DragOverElm.nextSibling); 155 | navContent.splice(overIndex + 1 >= child.length ? overIndex : overIndex + 1, 0, curData); 156 | navContent.splice(currentIndex, 1); 157 | } 158 | dbs.nav = navContent; 159 | storage.set({ dbs }); 160 | } 161 | currentIndex = null; 162 | overIndex = null; 163 | } 164 | render() { 165 | const { navContent, optionDown } = this.state; 166 | return ( 167 |
168 |
169 |
170 | {navContent.map((item, idx) => { 171 | const propsChild = { 172 | key: idx, 173 | target: '_top', 174 | draggable: true, 175 | className: classNames({ 176 | doc: item.type === 'doc', 177 | }), 178 | onContextMenu: this.onContextMenu.bind(this), 179 | onDragStart: this.onDragStart.bind(this), 180 | onDragEnd: this.onDragEnd.bind(this), 181 | onDragOver: this.onDragOver.bind(this), 182 | }; 183 | const child = ( 184 | 185 | {item.label} e.target.src = websiteIcon} src={item.icon} /> 186 |

{item.label}

187 | {optionDown && } 188 |
189 | ); 190 | propsChild.href = optionDown ? '#' : item.value; 191 | return ( 192 | {child} 193 | ); 194 | })} 195 | {navContent.length < 18 && ( 196 | // eslint-disable-next-line jsx-a11y/anchor-is-valid 197 | 198 | 199 | 200 | )} 201 |
202 |
203 | this.edit = comp } onClickAdd={this.onClickAdd.bind(this)} /> 204 |
205 | ); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/pages/Navigation/index.module.less: -------------------------------------------------------------------------------- 1 | .nav { 2 | position: relative; 3 | overflow: hidden; 4 | height: 100%; 5 | display: flex; 6 | width: 100%; 7 | flex-direction: column; 8 | user-select: none; 9 | .navBox { 10 | flex: 1; 11 | align-items: center; 12 | display: flex; 13 | } 14 | } 15 | 16 | .navContent { 17 | display: flex; 18 | position: relative; 19 | left: 50%; 20 | flex-flow: row wrap; 21 | align-content: flex-start; 22 | transform: translate(-50%,0); 23 | a { 24 | display: block; 25 | position: relative; 26 | cursor: pointer; 27 | text-decoration: none; 28 | padding: 0 38px; 29 | outline: 0; 30 | top: 0; 31 | left: 0; 32 | border: 2px solid transparent; 33 | border-radius: 4px; 34 | text-align: center; 35 | box-sizing: border-box; 36 | // &:global(.is-drod) { 37 | // // background-color: #333; 38 | // // opacity: 0.2; 39 | // } 40 | &::before { 41 | content: ''; 42 | display: block; 43 | width: 100%; 44 | height: 100%; 45 | position: absolute; 46 | z-index: 9; 47 | left: 0; 48 | } 49 | &:global(.doc) img { 50 | padding: 10px; 51 | } 52 | p { 53 | text-align: center; 54 | white-space: nowrap; 55 | overflow: hidden; 56 | text-overflow: ellipsis; 57 | color: var(--color-theme-text); 58 | } 59 | &:hover > img { 60 | filter:brightness(0.96); 61 | } 62 | &:active > img { 63 | filter:brightness(0.66); 64 | } 65 | &.addIcon { 66 | & > img { 67 | box-shadow: 0 0 0 0; 68 | } 69 | } 70 | } 71 | img { 72 | box-shadow: 0 2px 6px rgba(0,0,0,.15); 73 | margin-top: 8%; 74 | border-radius: 8px; 75 | width: 100%; 76 | display: block; 77 | filter: brightness(0.99); 78 | transition: filter .3s; 79 | background: #ffffff14; 80 | } 81 | .keyDown { 82 | height: 23px; 83 | width: 23px; 84 | border-radius: 50%; 85 | display: block; 86 | position: absolute; 87 | background-color: #fff; 88 | border: 1px solid rgba(27, 31, 35, 0.15); 89 | box-shadow: 0 3px 12px rgba(27, 31, 35, 0.15); 90 | top: 0; 91 | right: 0; 92 | z-index: 19; 93 | &::before, &::after { 94 | content: ''; 95 | display: block; 96 | background-color: #333; 97 | position: absolute; 98 | border-radius: 3px; 99 | height: 2px; 100 | width: 60%; 101 | top: 10px; 102 | left: 4px; 103 | } 104 | &::before { 105 | transform: rotate(45deg); 106 | } 107 | &::after { 108 | transform: rotate(-45deg); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/pages/Search/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import classNames from 'classnames'; 3 | import Search from '../../component/Search'; 4 | import searchdb from '../../source/search.json'; 5 | import styles from './index.module.less'; 6 | import Loading from '../../component/Loading'; 7 | 8 | export default class SearchView extends Component { 9 | static typeName = 'search'; 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | searchNav: searchdb || [], 14 | select: props.conf.selectType, 15 | value: props.conf.selectSubType, 16 | iframe: false, 17 | iframeUrl: '', // 设置 IFrame 打开的连接地址 18 | query: '', 19 | loading: false, 20 | }; 21 | } 22 | componentDidMount() { 23 | this.setStateLoading(); 24 | } 25 | setStateLoading() { 26 | const { searchNav, value, select, query, iframeUrl } = this.state; 27 | const option = this.getSubNavData(); 28 | const optionItem = option.filter(item => item.value === value)[0] || option[0]; 29 | const optionMenu = searchNav.filter(item => item.value === select)[0] || {}; 30 | const isTab = optionMenu && optionMenu.reveal && optionMenu.reveal === 'tab'; 31 | if ((optionItem.iframe !== false && query && !this.isSreach) || iframeUrl || isTab) { 32 | this.setState({ loading: true }); 33 | } 34 | } 35 | onClick(item) { 36 | const { storage, conf } = this.props; 37 | conf.selectType = item.value; 38 | storage.set({ conf }); 39 | this.setState({ select: item.value, iframeUrl: '', loading: false }, () => { 40 | if (item.children && item.children.length > 0) { 41 | const value = item.children[0].value; 42 | conf.selectSubType = value; 43 | storage.set({ conf }); 44 | this.setState({ value }); 45 | this.setStateLoading(); 46 | } 47 | if (this.search && this.search.input) { 48 | this.search.input.focus(); 49 | } 50 | }); 51 | } 52 | onClickSubTab(item) { 53 | if (item.target === '_blank') { 54 | return window.open(item.url); 55 | } 56 | const { storage, conf } = this.props; 57 | conf.selectSubType = item.value; 58 | storage.set({ conf }); 59 | this.setState({ value: item.value, loading: true }); 60 | } 61 | getSubNavData() { 62 | const { select, searchNav } = this.state; 63 | const menu = searchNav.filter(item => item.value === select); 64 | if (menu && menu.length > 0) { 65 | return menu[0].children || []; 66 | } 67 | return []; 68 | } 69 | onSearch(item) { 70 | this.isSreach = false; 71 | const { query } = this.state; 72 | const iframeUrl = item.url.replace(/\{\{.*\}\}/, query); 73 | if (item.iframe === false) { 74 | return window.open(iframeUrl); 75 | } 76 | this.setState({ iframe: true, iframeUrl, loading: true }); 77 | } 78 | onChange(e) { 79 | this.isSreach = true; 80 | this.setState({ query: e.target.value }); 81 | } 82 | onSelect(item) { 83 | const { query } = this.state; 84 | if (item.target === '_blank') { 85 | item.url = item.url.replace(/\{\{.*\}\}/, query); 86 | return window.open(item.url); 87 | } 88 | 89 | const { storage, conf } = this.props; 90 | conf.selectSubType = item.value; 91 | storage.set({ conf }); 92 | 93 | if (item.iframe === false) { 94 | this.isSreach = true; 95 | this.setState({ 96 | iframeUrl: '', 97 | }); 98 | } 99 | this.setState({ value: item.value }); 100 | } 101 | onFrameLoad() { 102 | this.setState({ loading: false }); 103 | } 104 | onKeyUp(optionItem, e) { 105 | const key = e.keyCode || e.which || e.charCode; 106 | if (key === 13) { // 摁Enter 107 | this.onSearch(optionItem); 108 | } 109 | } 110 | render() { 111 | const { select, value, query, searchNav, iframeUrl } = this.state; 112 | const option = this.getSubNavData(); 113 | const optionMenu = searchNav.filter(item => item.value === select)[0] || {}; 114 | const optionItem = option.filter(item => item.value === value)[0] || option[0]; 115 | const url = (optionItem.url || '').replace(/\{\{.*\}\}/, query); 116 | const isTab = optionMenu && optionMenu.reveal && optionMenu.reveal === 'tab'; 117 | const TabView = ( 118 |
119 | {option.map((item, idx) => ( 120 |
{item.label}
121 | ))} 122 | 123 |
124 | ); 125 | return ( 126 |
127 |
128 | {searchNav.map((item, idx) => ( 129 |
130 | {item.label} 131 |
132 | ))} 133 |
134 | {isTab && TabView} 135 | {!isTab && ( 136 |
137 | this.search = searchCom} 140 | placeholder="请输入文字" 141 | query={query} 142 | onChange={this.onChange.bind(this)} 143 | onSearch={this.onSearch.bind(this, optionItem)} 144 | onKeyUp={this.onKeyUp.bind(this, optionItem)} 145 | select={{ 146 | option, 147 | value, 148 | onSelect: this.onSelect.bind(this), 149 | }} 150 | /> 151 | 152 |
153 | )} 154 | {((optionItem.iframe !== false && query && !this.isSreach) || iframeUrl || isTab) && ( 155 |
156 |