diff --git a/README.md b/README.md index 9fc88aa..4feff80 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ If you are using macOS or an older version of Windows, you can also [get Fluent ### Contribute -Make Fluent Reader better by reporting bugs or opening feature requests through [GitHub issues](https://github.com/yang991178/fluent-reader/issues). +Help make Fluent Reader better by reporting bugs or opening feature requests through [GitHub issues](https://github.com/yang991178/fluent-reader/issues). You can also help internationalize the app by providing [translations into additional languages](https://github.com/yang991178/fluent-reader/tree/master/src/scripts/i18n). Refer to the repo of [react-intl-universal](https://github.com/alibaba/react-intl-universal) to get started on internationalization. diff --git a/dist/styles.css b/dist/styles.css index 52d8e2f..be7999a 100644 --- a/dist/styles.css +++ b/dist/styles.css @@ -45,9 +45,6 @@ html, body { overflow: hidden; margin: 0; } -body { - background-color: var(--neutralLighterAlt); -} #root { height: 100%; } @@ -157,6 +154,9 @@ nav .progress { text-align: center; vertical-align: middle; } +body.darwin .btn-group .seperator { + line-height: 32px; +} .btn-group .seperator::before { content: "|"; } @@ -184,12 +184,15 @@ nav.menu-on .btn-group .btn.system, nav.hide-btns .btn-group .btn.system { nav.menu-on .btn-group .btn.system, nav.item-on .btn-group .btn.system { color: var(--white); } -.btn-group .btn:hover { +.btn-group .btn:hover, .ms-Nav-compositeLink:hover { background-color: #0001; } -.btn-group .btn:active { +.btn-group .btn:active, .ms-Nav-compositeLink:active { background-color: #0002; } +.ms-Nav-compositeLink:hover .ms-Nav-link { + background: none; +} .btn-group .btn.disabled, .btn-group .btn.fetching { background-color: unset !important; color: var(--neutralSecondaryAlt); @@ -263,7 +266,7 @@ nav.menu-on .btn-group .btn.system, nav.item-on .btn-group .btn.system { .settings-container { position: fixed; - z-index: 5; + z-index: 7; left: 0; top: 0; width: 100%; @@ -295,7 +298,7 @@ div[role="tabpanel"] { width: 100%; height: 100%; background-color: #fffa; - z-index: 6; + z-index: 8; } .settings .loading .ms-Spinner { margin-top: 180px; @@ -346,16 +349,18 @@ img.favicon { } .main { - height: calc(100% - 32px); - overflow-y: scroll; + margin-top: -32px; + height: 100%; + overflow: hidden; + background-color: var(--neutralLighterAlt); } .main::before { content: ""; display: block; position: sticky; - top: 0; + top: 32px; left: 0; - width: 100%; + width: calc(100% - 16px); height: 32px; margin-bottom: -32px; background: linear-gradient(var(--neutralLighterAlt), #faf9f800); @@ -390,10 +395,15 @@ img.favicon { } .menu-container { width: 280px; + background: none; + backdrop-filter: none; } .menu-container .menu { background-color: var(--neutralLight); } + body.darwin .menu-container .menu { + background: none; + } .menu-container .menu::after { content: ""; display: block; @@ -564,6 +574,9 @@ img.favicon { flex-wrap: wrap; justify-content: space-around; padding: 12px; + height: calc(100% - 56px); + overflow: hidden scroll; + margin-top: 32px; } .cards-feed-container > div.load-more-wrapper, .flex-fix { text-align: center; @@ -654,11 +667,13 @@ img.favicon { cursor: pointer; animation-fill-mode: none; } -.card:hover, .card:focus { - box-shadow: #0006 0px 5px 40px; +.card:focus, .list-card:focus { outline: none; } -.card:focus::after, .list-card:focus::after { +.card:hover, .ms-Fabric--isFocusVisible .card:focus { + box-shadow: #0006 0px 5px 40px; +} +.ms-Fabric--isFocusVisible .card:focus::after, .ms-Fabric--isFocusVisible .list-card:focus::after { content: ""; position: absolute; top: 2px; @@ -698,7 +713,9 @@ img.favicon { transition: transform ease-out .12s; } .card.transform:hover img.head, .card.transform:hover p, .card.transform:hover h3, -.card.transform:focus img.head, .card.transform:focus p, .card.transform:focus h3 { +.ms-Fabric--isFocusVisible .card.transform:focus img.head, +.ms-Fabric--isFocusVisible .card.transform:focus p, +.ms-Fabric--isFocusVisible .card.transform:focus h3 { transform: translateY(-144px); } .card h3.title { @@ -752,9 +769,8 @@ img.favicon { cursor: pointer; box-shadow: #0000 0px 5px 15px; } -.list-card:hover, .list-card:focus { +.list-card:hover, .ms-Fabric--isFocusVisible .list-card:focus { box-shadow: #0004 0px 5px 15px; - outline: none; } .list-card:active { box-shadow: #0000 0px 5px 15px, inset #0004 0px 0px 15px; @@ -793,10 +809,10 @@ img.favicon { .ms-Button--commandBar.active .ms-Button-icon { color: #c7e0f4; } - .btn-group .btn:hover { + .btn-group .btn:hover, .ms-Nav-compositeLink:hover { background-color: #fff1; } - .btn-group .btn:active { + .btn-group .btn:active, .ms-Nav-compositeLink:active { background-color: #fff2; } .settings .loading { diff --git a/package.json b/package.json index 52678d5..774240e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fluent-reader", - "version": "0.3.2", + "version": "0.3.3", "description": "A simplistic, modern desktop RSS reader", "main": "./dist/electron.js", "scripts": { diff --git a/src/components/article.tsx b/src/components/article.tsx index 6917c58..b9f8098 100644 --- a/src/components/article.tsx +++ b/src/components/article.tsx @@ -31,7 +31,6 @@ type ArticleState = { class Article extends React.Component { webview: Electron.WebviewTag - shouldRefocus = false constructor(props) { super(props) @@ -107,18 +106,28 @@ class Article extends React.Component { if (input.type === "keyDown") { switch (input.key) { case "Escape": - this.shouldRefocus = true this.props.dismiss() break case "ArrowLeft": case "ArrowRight": this.props.offsetItem(input.key === "ArrowLeft" ? -1 : 1) break - case "l": + case "l": case "L": this.toggleWebpage() break default: this.props.shortcuts(this.props.item, input.key) + const keyboardEvent = new KeyboardEvent("keydown", { + code: input.code, + key: input.key, + shiftKey: input.shift, + altKey: input.alt, + ctrlKey: input.control, + metaKey: input.meta, + repeat: input.isAutoRepeat, + bubbles: true + }) + document.dispatchEvent(keyboardEvent) break } } @@ -149,10 +158,8 @@ class Article extends React.Component { } componentWillUnmount = () => { - if (this.shouldRefocus) { - let refocus = document.querySelector(`#refocus>div[data-iid="${this.props.item._id}"]`) as HTMLElement - if (refocus) refocus.focus() - } + let refocus = document.querySelector(`#refocus>div[data-iid="${this.props.item._id}"]`) as HTMLElement + if (refocus) refocus.focus() } openInBrowser = () => { diff --git a/src/components/cards/default-card.tsx b/src/components/cards/default-card.tsx index ac9a68c..0d481f6 100644 --- a/src/components/cards/default-card.tsx +++ b/src/components/cards/default-card.tsx @@ -17,7 +17,6 @@ class DefaultCard extends Card { className={this.className()} onClick={this.onClick} onMouseUp={this.onMouseUp} - onMouseDown={event => event.preventDefault()} onKeyDown={this.onKeyDown} data-iid={this.props.item._id} data-is-focusable> diff --git a/src/components/cards/list-card.tsx b/src/components/cards/list-card.tsx index a4cb4b5..3ef1a8c 100644 --- a/src/components/cards/list-card.tsx +++ b/src/components/cards/list-card.tsx @@ -16,7 +16,6 @@ class ListCard extends Card { className={this.className()} onClick={this.onClick} onMouseUp={this.onMouseUp} - onMouseDown={event => event.preventDefault()} onKeyDown={this.onKeyDown} data-iid={this.props.item._id} data-is-focusable> diff --git a/src/components/menu.tsx b/src/components/menu.tsx index 0dcb8e3..c67707b 100644 --- a/src/components/menu.tsx +++ b/src/components/menu.tsx @@ -91,8 +91,10 @@ export class Menu extends React.Component { let [type, index] = item.key.split("-") if (type === "s") { sids = [parseInt(index)] - } else { + } else if (type === "g") { sids = this.props.groups[parseInt(index)].sids + } else { + return } this.props.groupContextMenu(sids, event) } diff --git a/src/components/nav.tsx b/src/components/nav.tsx index 699f829..f589150 100644 --- a/src/components/nav.tsx +++ b/src/components/nav.tsx @@ -7,14 +7,15 @@ import { ProgressIndicator } from "@fluentui/react" import { getWindowBreakpoint } from "../scripts/utils" type NavProps = { - state: AppState, - itemShown: boolean, - fetch: () => void, - menu: () => void, - logs: () => void, - views: () => void, - settings: () => void, + state: AppState + itemShown: boolean + menu: () => void + search: () => void markAllRead: () => void + fetch: () => void + logs: () => void + views: () => void + settings: () => void } type NavState = { @@ -44,6 +45,9 @@ class Nav extends React.Component { case "F1": this.props.menu() break + case "F2": + this.props.search() + break case "F5": this.fetch() break diff --git a/src/components/page.tsx b/src/components/page.tsx index 0ae354c..4befe09 100644 --- a/src/components/page.tsx +++ b/src/components/page.tsx @@ -7,6 +7,7 @@ import ArticleSearch from "./utils/article-search" type PageProps = { menuOn: boolean + contextOn: boolean settingsOn: boolean feeds: string[] itemId: string @@ -35,6 +36,7 @@ class Page extends React.Component { } {this.props.itemId && ( { handleInputChange = (event) => { const name: string = event.target.name - this.setState({[name]: event.target.value.trim()}) + this.setState({[name]: event.target.value}) } createGroup = (event: React.FormEvent) => { event.preventDefault() - if (this.state.newGroupName.length > 0) this.props.createGroup(this.state.newGroupName) + let trimmed = this.state.newGroupName.trim() + if (trimmed.length > 0) this.props.createGroup(trimmed) } addToGroup = () => { @@ -239,7 +240,7 @@ class GroupsTab extends React.Component { updateGroupName = () => { let group = this.state.selectedGroup - group = { ...group, name: this.state.editGroupName } + group = { ...group, name: this.state.editGroupName.trim() } this.props.updateGroup(group) } @@ -292,7 +293,7 @@ class GroupsTab extends React.Component { @@ -325,7 +326,7 @@ class GroupsTab extends React.Component { diff --git a/src/components/settings/sources.tsx b/src/components/settings/sources.tsx index 76a11e9..ac60248 100644 --- a/src/components/settings/sources.tsx +++ b/src/components/settings/sources.tsx @@ -101,12 +101,13 @@ class SourcesTab extends React.Component { handleInputChange = (event) => { const name: string = event.target.name - this.setState({[name]: event.target.value.trim()}) + this.setState({[name]: event.target.value}) } addSource = (event: React.FormEvent) => { event.preventDefault() - if (urlTest(this.state.newUrl)) this.props.addSource(this.state.newUrl) + let trimmed = this.state.newUrl.trim() + if (urlTest(trimmed)) this.props.addSource(trimmed) } onOpenTargetChange = (_, option: IChoiceGroupOption) => { @@ -142,7 +143,7 @@ class SourcesTab extends React.Component { @@ -171,8 +172,8 @@ class SourcesTab extends React.Component { this.props.updateSourceName(this.state.selectedSource, this.state.newSourceName)} + disabled={this.state.newSourceName.trim().length == 0} + onClick={() => this.props.updateSourceName(this.state.selectedSource, this.state.newSourceName.trim())} text={intl.get("sources.editName")} /> diff --git a/src/components/utils/article-search.tsx b/src/components/utils/article-search.tsx index fac0e98..b2a68eb 100644 --- a/src/components/utils/article-search.tsx +++ b/src/components/utils/article-search.tsx @@ -41,6 +41,7 @@ class ArticleSearch extends React.Component { componentDidUpdate(prevProps: SearchProps) { if (this.props.searchOn && !prevProps.searchOn) { + this.setState({ query: this.props.initQuery }) this.inputRef.current.focus() } } diff --git a/src/containers/nav-container.tsx b/src/containers/nav-container.tsx index 5048ba9..f40c231 100644 --- a/src/containers/nav-container.tsx +++ b/src/containers/nav-container.tsx @@ -5,7 +5,7 @@ import { createSelector } from "reselect" import { RootState } from "../scripts/reducer" import { fetchItems, markAllRead } from "../scripts/models/item" import { toggleMenu, toggleLogMenu, toggleSettings, openViewMenu } from "../scripts/models/app" -import { ViewType } from "../scripts/models/page" +import { ViewType, toggleSearch } from "../scripts/models/page" import Nav from "../components/nav" const getState = (state: RootState) => state.app @@ -25,6 +25,7 @@ const mapDispatchToProps = (dispatch) => ({ logs: () => dispatch(toggleLogMenu()), views: () => dispatch(openViewMenu()), settings: () => dispatch(toggleSettings()), + search: () => dispatch(toggleSearch()), markAllRead: () => { remote.dialog.showMessageBox(remote.getCurrentWindow(), { title: intl.get("nav.markAllRead"), diff --git a/src/containers/page-container.tsx b/src/containers/page-container.tsx index 15cfeed..548a3e7 100644 --- a/src/containers/page-container.tsx +++ b/src/containers/page-container.tsx @@ -4,17 +4,20 @@ import { RootState } from "../scripts/reducer" import Page from "../components/page" import { AppDispatch } from "../scripts/utils" import { dismissItem, showOffsetItem } from "../scripts/models/page" +import { ContextMenuType } from "../scripts/models/app" const getPage = (state: RootState) => state.page const getSettings = (state: RootState) => state.app.settings.display const getMenu = (state: RootState) => state.app.menu +const getContext = (state: RootState) => state.app.contextMenu.type != ContextMenuType.Hidden const mapStateToProps = createSelector( - [getPage, getSettings, getMenu], - (page, settingsOn, menuOn) => ({ + [getPage, getSettings, getMenu, getContext], + (page, settingsOn, menuOn, contextOn) => ({ feeds: [page.feedId], settingsOn: settingsOn, menuOn: menuOn, + contextOn: contextOn, itemId: page.itemId, viewType: page.viewType }) diff --git a/src/electron.ts b/src/electron.ts index 1d1c029..55c926f 100644 --- a/src/electron.ts +++ b/src/electron.ts @@ -29,7 +29,8 @@ function createWindow() { // Create the browser window. mainWindow = new BrowserWindow({ title: "Fluent Reader", - backgroundColor: nativeTheme.shouldUseDarkColors ? "#282828" : "#faf9f8", + backgroundColor: process.platform === "darwin" ? "#00000000" : (nativeTheme.shouldUseDarkColors ? "#282828" : "#faf9f8"), + vibrancy: "sidebar", x: mainWindowState.x, y: mainWindowState.y, width: mainWindowState.width, @@ -55,7 +56,7 @@ function createWindow() { mainWindow.loadFile((app.isPackaged ? "dist/" : "") + "index.html") } -if (process.platform === 'darwin') { +if (process.platform === "darwin") { const template = [ { label: "Application", @@ -66,8 +67,12 @@ if (process.platform === 'darwin') { { label: "Edit", submenu: [ + { label: "Undo", accelerator: "CmdOrCtrl+Z", selector: "undo:" }, + { label: "Redo", accelerator: "Shift+CmdOrCtrl+Z", selector: "redo:" }, + { label: "Cut", accelerator: "CmdOrCtrl+X", selector: "cut:" }, { label: "Copy", accelerator: "CmdOrCtrl+C", selector: "copy:" }, { label: "Paste", accelerator: "CmdOrCtrl+V", selector: "paste:" }, + { label: "Select All", accelerator: "CmdOrCtrl+A", selector: "selectAll:" } ] } ] diff --git a/src/scripts/models/app.ts b/src/scripts/models/app.ts index 3f286a8..e36afc5 100644 --- a/src/scripts/models/app.ts +++ b/src/scripts/models/app.ts @@ -199,6 +199,7 @@ export function initIntl(): AppThunk> { export function initApp(): AppThunk { return (dispatch) => { + document.body.classList.add(process.platform) dispatch(initIntl()).then(() => dispatch(initSources()) ).then(() => @@ -266,10 +267,7 @@ export function appReducer( } case INIT_FEEDS: switch (action.status) { - case ActionStatus.Request: return { - ...state, - feedInit: false - } + case ActionStatus.Request: return state default: return { ...state, feedInit: true diff --git a/src/scripts/models/item.ts b/src/scripts/models/item.ts index e689943..c5f9c88 100644 --- a/src/scripts/models/item.ts +++ b/src/scripts/models/item.ts @@ -266,17 +266,18 @@ export function toggleHidden(item: RSSItem): AppThunk { export function itemShortcuts(item: RSSItem, key: string): AppThunk { return (dispatch) => { switch (key) { - case "m": + case "m": case "M": if (item.hasRead) dispatch(markUnread(item)) else dispatch(markRead(item)) break - case "b": + case "b": case "B": + if (!item.hasRead) dispatch(markRead(item)) openExternal(item.link) break - case "s": + case "s": case "S": dispatch(toggleStarred(item)) break - case "h": + case "h": case "H": dispatch(toggleHidden(item)) break } diff --git a/src/scripts/models/page.ts b/src/scripts/models/page.ts index 7b9696f..6f3a0a4 100644 --- a/src/scripts/models/page.ts +++ b/src/scripts/models/page.ts @@ -1,5 +1,5 @@ -import { ALL, SOURCE, loadMore, FeedFilter, FilterType, initFeeds } from "./feed" -import { getWindowBreakpoint, AppThunk } from "../utils" +import { ALL, SOURCE, loadMore, FeedFilter, FilterType, initFeeds, FeedActionTypes, INIT_FEED } from "./feed" +import { getWindowBreakpoint, AppThunk, ActionStatus } from "../utils" import { getDefaultView } from "../settings" import { RSSItem, markRead } from "./item" import { SourceActionTypes, DELETE_SOURCE } from "./source" @@ -104,7 +104,7 @@ export const toggleSearch = (): AppThunk => { return (dispatch, getState) => { let state = getState() dispatch(({ type: TOGGLE_SEARCH })) - if (!getWindowBreakpoint()) { + if (!getWindowBreakpoint() && state.app.menu) { dispatch(toggleMenu()) } if (state.page.searchOn) { @@ -214,7 +214,7 @@ export class PageState { export function pageReducer( state = new PageState(), - action: PageActionTypes | SourceActionTypes + action: PageActionTypes | SourceActionTypes | FeedActionTypes ): PageState { switch (action.type) { case SELECT_PAGE: @@ -244,6 +244,14 @@ export function pageReducer( ...state, itemId: action.item._id } + case INIT_FEED: switch (action.status) { + case ActionStatus.Success: return { + ...state, + itemId: (action.feed._id === state.feedId && action.items.filter(i => i._id === state.itemId).length === 0) + ? null : state.itemId + } + default: return state + } case DELETE_SOURCE: case DISMISS_ITEM: return { ...state,