diff --git a/dist/article/article.css b/dist/article/article.css index b0bf21a..015cd8b 100644 --- a/dist/article/article.css +++ b/dist/article/article.css @@ -13,8 +13,8 @@ body { body.dark { color: #f8f8f8; --gray: #a19f9d; - --primary: #3a96dd; - --primary-alt: #4ba0e1; + --primary: #4ba0e1; + --primary-alt: #65aee6; } a { diff --git a/dist/styles.css b/dist/styles.css index c6671f2..a6d33e3 100644 --- a/dist/styles.css +++ b/dist/styles.css @@ -36,6 +36,7 @@ body.dark { } html, body { + background-color: transparent; font-family: "Segoe UI Regular", "Source Han Sans SC Regular", "Microsoft YaHei", sans-serif; height: 100%; overflow: hidden; @@ -94,7 +95,7 @@ i.ms-Nav-chevron { .ms-Label, .ms-Spinner-label { user-select: none; } -.ms-ActivityItem { +.ms-ActivityItem, .ms-ActivityItem-commentText { color: var(--neutralSecondary); } .ms-ActivityItem-timeStamp { @@ -192,7 +193,7 @@ body.dark .btn-group .btn:active { background-color: #fff2; } .btn-group .btn.disabled, .btn-group .btn.fetching { - background-color: unset; + background-color: unset !important; color: var(--neutralSecondaryAlt); } .btn-group .btn.fetching { @@ -297,6 +298,9 @@ body.dark .settings .loading { .tab-body .ms-StackItem:last-child { margin-right: 0; } +.tab-body .ms-ChoiceFieldGroup { + margin-bottom: 20px; +} img.favicon { width: 16px; height: 16px; diff --git a/package.json b/package.json index f63fd3a..090adda 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "pac-proxy-agent": "^4.1.0", "react": "^16.13.1", "react-dom": "^16.13.1", + "react-intl-universal": "^2.2.5", "react-redux": "^7.2.0", "redux": "^4.0.5", "redux-devtools": "^3.5.0", diff --git a/src/components/root.tsx b/src/components/root.tsx index 7a511f1..5c40512 100644 --- a/src/components/root.tsx +++ b/src/components/root.tsx @@ -7,9 +7,11 @@ import MenuContainer from "../containers/menu-container" import NavContainer from "../containers/nav-container" import LogMenuContainer from "../containers/log-menu-container" import SettingsContainer from "../containers/settings-container" +import { RootState } from "../scripts/reducer" -const Root = ({ dispatch }) => ( +const Root = ({ locale, dispatch }) => locale && (
dispatch(closeContextMenu())} onContextMenu={event => { let text = document.getSelection().toString() @@ -24,4 +26,5 @@ const Root = ({ dispatch }) => (
) -export default connect()(Root) \ No newline at end of file +const getLocale = (state: RootState) => ({ locale: state.app.locale }) +export default connect(getLocale)(Root) \ No newline at end of file diff --git a/src/components/settings.tsx b/src/components/settings.tsx index 131d26d..04e1111 100644 --- a/src/components/settings.tsx +++ b/src/components/settings.tsx @@ -5,7 +5,7 @@ import AboutTab from "./settings/about" import { Pivot, PivotItem, Spinner } from "@fluentui/react" import SourcesTabContainer from "../containers/settings/sources-container" import GroupsTabContainer from "../containers/settings/groups-container" -import AppTab from "./settings/app" +import AppTabContainer from "../containers/settings/app-container" type SettingsProps = { display: boolean, @@ -38,7 +38,7 @@ class Settings extends React.Component { - + diff --git a/src/components/settings/app.tsx b/src/components/settings/app.tsx index 6d40fbf..6b8d0da 100644 --- a/src/components/settings/app.tsx +++ b/src/components/settings/app.tsx @@ -1,20 +1,31 @@ import * as React from "react" +import intl = require("react-intl-universal") import { urlTest } from "../../scripts/utils" -import { getProxy, getProxyStatus, toggleProxyStatus, setProxy, getThemeSettings, setThemeSettings, ThemeSettings } from "../../scripts/settings" -import { Stack, Label, Toggle, TextField, DefaultButton, ChoiceGroup, IChoiceGroupOption, loadTheme } from "@fluentui/react" +import { getProxy, getProxyStatus, toggleProxyStatus, setProxy, getThemeSettings, setThemeSettings, ThemeSettings, getLocaleSettings } from "../../scripts/settings" +import { Stack, Label, Toggle, TextField, DefaultButton, ChoiceGroup, IChoiceGroupOption, loadTheme, Dropdown, IDropdownOption } from "@fluentui/react" -const themeChoices: IChoiceGroupOption[] = [ - { key: ThemeSettings.Default, text: "系统默认" }, - { key: ThemeSettings.Light, text: "浅色模式" }, - { key: ThemeSettings.Dark, text: "深色模式" } -] +type AppTabProps = { + setLanguage: (option: string) => void +} -class AppTab extends React.Component { +class AppTab extends React.Component { state = { pacStatus: getProxyStatus(), pacUrl: getProxy(), themeSettings: getThemeSettings() } + + themeChoices = (): IChoiceGroupOption[] => [ + { key: ThemeSettings.Default, text: intl.get("followSystem") }, + { key: ThemeSettings.Light, text: intl.get("app.lightTheme") }, + { key: ThemeSettings.Dark, text: intl.get("app.darkTheme") } + ] + + languageOptions = (): IDropdownOption[] => [ + { key: "default", text: intl.get("followSystem") }, + { key: "en-US", text: "English" }, + { key: "zh-CN", text: "中文(简体)"} + ] toggleStatus = () => { toggleProxyStatus() @@ -41,9 +52,26 @@ class AppTab extends React.Component { render = () => (
+ + + + this.props.setLanguage(String(option.key))} + style={{width: 200}} /> + + + + + - + @@ -54,8 +82,8 @@ class AppTab extends React.Component { urlTest(v.trim()) ? "" : "请正确输入URL"} - placeholder="PAC地址" + onGetErrorMessage={v => urlTest(v.trim()) ? "" : intl.get("app.badUrl")} + placeholder={intl.get("app.pac")} name="pacUrl" onChange={this.handleInputChange} value={this.state.pacUrl} /> @@ -64,16 +92,10 @@ class AppTab extends React.Component { + text={intl.get("app.setPac")} /> } - -
) } diff --git a/src/components/settings/sources.tsx b/src/components/settings/sources.tsx index f885572..a74fb1e 100644 --- a/src/components/settings/sources.tsx +++ b/src/components/settings/sources.tsx @@ -1,4 +1,5 @@ import * as React from "react" +import intl = require("react-intl-universal") import { Label, DefaultButton, TextField, Stack, PrimaryButton, DetailsList, IColumn, SelectionMode, Selection, IChoiceGroupOption, ChoiceGroup } from "@fluentui/react" import { SourceState, RSSSource, SourceOpenTarget } from "../../scripts/models/source" @@ -20,42 +21,6 @@ type SourcesTabState = { selectedSource: RSSSource } -const columns: IColumn[] = [ - { - key: "favicon", - name: "图标", - fieldName: "name", - isIconOnly: true, - iconName: "ImagePixel", - minWidth: 16, - maxWidth: 16, - onRender: (s: RSSSource) => s.iconurl && ( - - ) - }, - { - key: "name", - name: "名称", - fieldName: "name", - minWidth: 200, - data: 'string', - isRowHeader: true - }, - { - key: "url", - name: "URL", - fieldName: "url", - minWidth: 280, - data: 'string' - } -] - -const sourceOpenTargetChoices: IChoiceGroupOption[] = [ - { key: String(SourceOpenTarget.Local), text: "RSS正文" }, - { key: String(SourceOpenTarget.Webpage), text: "加载网页" }, - { key: String(SourceOpenTarget.External), text: "在浏览器中打开" } -] - class SourcesTab extends React.Component { selection: Selection @@ -78,6 +43,42 @@ class SourcesTab extends React.Component { }) } + columns = (): IColumn[] => [ + { + key: "favicon", + name: intl.get("icon"), + fieldName: "name", + isIconOnly: true, + iconName: "ImagePixel", + minWidth: 16, + maxWidth: 16, + onRender: (s: RSSSource) => s.iconurl && ( + + ) + }, + { + key: "name", + name: intl.get("name"), + fieldName: "name", + minWidth: 200, + data: 'string', + isRowHeader: true + }, + { + key: "url", + name: "URL", + fieldName: "url", + minWidth: 280, + data: 'string' + } + ] + + sourceOpenTargetChoices = (): IChoiceGroupOption[] => [ + { key: String(SourceOpenTarget.Local), text: intl.get("sources.rssText") }, + { key: String(SourceOpenTarget.Webpage), text: intl.get("sources.loadWebpage") }, + { key: String(SourceOpenTarget.External), text: intl.get("openExternal") } + ] + handleInputChange = (event) => { const name: string = event.target.name this.setState({[name]: event.target.value.trim()}) @@ -96,24 +97,24 @@ class SourcesTab extends React.Component { render = () => (
- + - + - +
- + urlTest(v.trim()) ? "" : "请正确输入URL"} + onGetErrorMessage={v => urlTest(v.trim()) ? "" : intl.get("sources.badUrl")} validateOnLoad={false} - placeholder="输入URL" + placeholder={intl.get("sources.inputUrl")} value={this.state.newUrl} id="newUrl" name="newUrl" @@ -123,27 +124,27 @@ class SourcesTab extends React.Component { + text={intl.get("add")} />
s.sid} setKey="selected" selection={this.selection} selectionMode={SelectionMode.single} /> {this.state.selectedSource && <> - + v.trim().length == 0 ? "名称不得为空" : ""} + onGetErrorMessage={v => v.trim().length == 0 ? intl.get("emptyName") : ""} validateOnLoad={false} - placeholder="订阅源名称" + placeholder={intl.get("sources.name")} value={this.state.newSourceName} name="newSourceName" onChange={this.handleInputChange} /> @@ -152,23 +153,23 @@ class SourcesTab extends React.Component { this.props.updateSourceName(this.state.selectedSource, this.state.newSourceName)} - text="修改名称" /> + text={intl.get("sources.editName")} /> - + this.props.deleteSource(this.state.selectedSource)} key={this.state.selectedSource.sid} - text={`删除订阅源`} /> + text={intl.get("sources.delete")} /> - 这将移除此订阅源与所有已保存的文章 + {intl.get("sources.deleteWarning")} } diff --git a/src/containers/settings/app-container.tsx b/src/containers/settings/app-container.tsx new file mode 100644 index 0000000..fb0064d --- /dev/null +++ b/src/containers/settings/app-container.tsx @@ -0,0 +1,14 @@ +import { connect } from "react-redux" +import { setLocaleSettings } from "../../scripts/settings" +import { initIntl } from "../../scripts/models/app" +import AppTab from "../../components/settings/app" + +const mapDispatchToProps = dispatch => ({ + setLanguage: (option: string) => { + setLocaleSettings(option) + dispatch(initIntl()) + } +}) + +const AppTabContainer = connect(null, mapDispatchToProps)(AppTab) +export default AppTabContainer \ No newline at end of file diff --git a/src/electron.ts b/src/electron.ts index 4aa00e2..e55728b 100644 --- a/src/electron.ts +++ b/src/electron.ts @@ -1,9 +1,10 @@ import { app, ipcMain, BrowserWindow, Menu, nativeTheme } from "electron" import windowStateKeeper = require("electron-window-state") -import Store = require('electron-store'); +import Store = require("electron-store") let mainWindow: BrowserWindow const store = new Store() +nativeTheme.themeSource = store.get("theme", "system") function createWindow() { let mainWindowState = windowStateKeeper({ @@ -13,7 +14,7 @@ function createWindow() { // Create the browser window. mainWindow = new BrowserWindow({ title: "Fluent Reader", - backgroundColor: shouldUseDarkColors() ? "#282828" : "#faf9f8", + backgroundColor: nativeTheme.shouldUseDarkColors ? "#282828" : "#faf9f8", x: mainWindowState.x, y: mainWindowState.y, width: mainWindowState.width, @@ -22,15 +23,20 @@ function createWindow() { minHeight: 600, frame: false, fullscreenable: false, + show: false, webPreferences: { nodeIntegration: true, webviewTag: true } }) mainWindowState.manage(mainWindow) + mainWindow.on('ready-to-show', () => { + mainWindow.show() + mainWindow.focus() + mainWindow.webContents.openDevTools() + }); // and load the index.html of the app. mainWindow.loadFile((app.isPackaged ? "dist/" : "") + 'index.html') - mainWindow.webContents.openDevTools() } Menu.setApplicationMenu(null) @@ -54,10 +60,3 @@ ipcMain.on("set-theme", (_, theme) => { store.set("theme", theme) nativeTheme.themeSource = theme }) - -function shouldUseDarkColors() { - let option = store.get("theme", "system") - return option === "system" - ? nativeTheme.shouldUseDarkColors - : option === "dark" -} \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx index fa5029a..7844373 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,15 +3,12 @@ import * as ReactDOM from "react-dom" import { Provider } from "react-redux" import { createStore, applyMiddleware } from "redux" import thunkMiddleware from "redux-thunk" -import { loadTheme } from '@fluentui/react' import { initializeIcons } from "@fluentui/react/lib/Icons" import { rootReducer, RootState } from "./scripts/reducer" -import { initSources } from "./scripts/models/source" -import { fetchItems } from "./scripts/models/item" import Root from "./components/root" -import { initFeeds } from "./scripts/models/feed" import { AppDispatch } from "./scripts/utils" import { setProxy, applyThemeSettings } from "./scripts/settings" +import { initApp } from "./scripts/models/app" setProxy() @@ -23,7 +20,7 @@ const store = createStore( applyMiddleware(thunkMiddleware) ) -store.dispatch(initSources()).then(() => store.dispatch(initFeeds())).then(() => store.dispatch(fetchItems())) +store.dispatch(initApp()) ReactDOM.render( diff --git a/src/scripts/i18n/_locales.ts b/src/scripts/i18n/_locales.ts new file mode 100644 index 0000000..abcfbd4 --- /dev/null +++ b/src/scripts/i18n/_locales.ts @@ -0,0 +1,9 @@ +import en_US from "./en-US.json" +import zh_CN from "./zh-CN.json" + +const locales = { + "en-US": en_US, + "zh-CN": zh_CN, +} + +export default locales \ No newline at end of file diff --git a/src/scripts/i18n/en-US.json b/src/scripts/i18n/en-US.json new file mode 100644 index 0000000..bb673ba --- /dev/null +++ b/src/scripts/i18n/en-US.json @@ -0,0 +1,36 @@ +{ + "allArticles": "All articles", + "add": "Add", + "create": "Create", + "icon": "Icon", + "name": "Name", + "openExternal": "Open externally", + "emptyName": "This field cannot be empty", + "followSystem": "Follow system", + "sources": { + "opmlFile": "OPML File", + "name": "Source name", + "editName": "Edit name", + "openTarget": "Default open target for articles", + "delete": "Delete source", + "add": "Add source", + "import": "Import", + "export": "Export", + "rssText": "RSS full text", + "loadWebpage": "Load webpage", + "inputUrl": "Enter URL", + "badUrl": "Invalid URL", + "deleteWarning": "The source and all saved articles will be removed", + "selected": "Selected source" + }, + "app": { + "language": "Display language", + "theme": "Theme", + "lightTheme": "Light mode", + "darkTheme": "Dark mode", + "enableProxy": "Enable Proxy", + "badUrl": "Invalid URL", + "pac": "PAC Address", + "setPac": "Set PAC" + } +} \ No newline at end of file diff --git a/src/scripts/i18n/zh-CN.json b/src/scripts/i18n/zh-CN.json new file mode 100644 index 0000000..1440230 --- /dev/null +++ b/src/scripts/i18n/zh-CN.json @@ -0,0 +1,36 @@ +{ + "allArticles": "全部文章", + "add": "添加", + "create": "新建", + "icon": "图标", + "name": "名称", + "openExternal": "在浏览器中打开", + "emptyName": "名称不得为空", + "followSystem": "跟随系统", + "sources": { + "opmlFile": "OPML文件", + "name": "订阅源名称", + "editName": "修改名称", + "openTarget": "订阅源文章打开方式", + "delete": "删除订阅源", + "add": "添加订阅源", + "import": "导入文件", + "export": "导出文件", + "rssText": "RSS正文", + "loadWebpage": "加载网页", + "inputUrl": "输入URL", + "badUrl": "请正确输入URL", + "deleteWarning": "这将移除此订阅源与所有已保存的文章", + "selected": "选中订阅源" + }, + "app": { + "language": "界面语言", + "theme": "应用主题", + "lightTheme": "浅色模式", + "darkTheme": "深色模式", + "enableProxy": "启用代理", + "badUrl": "请正确输入URL", + "pac": "PAC地址", + "setPac": "设置PAC" + } +} \ No newline at end of file diff --git a/src/scripts/models/app.ts b/src/scripts/models/app.ts index 8ebf162..5186756 100644 --- a/src/scripts/models/app.ts +++ b/src/scripts/models/app.ts @@ -1,9 +1,12 @@ -import { RSSSource, INIT_SOURCES, SourceActionTypes, ADD_SOURCE, UPDATE_SOURCE, DELETE_SOURCE } from "./source" -import { RSSItem, ItemActionTypes, FETCH_ITEMS } from "./item" +import intl = require("react-intl-universal") +import { RSSSource, INIT_SOURCES, SourceActionTypes, ADD_SOURCE, UPDATE_SOURCE, DELETE_SOURCE, initSources } from "./source" +import { RSSItem, ItemActionTypes, FETCH_ITEMS, fetchItems } from "./item" import { ActionStatus, AppThunk, getWindowBreakpoint } from "../utils" import { INIT_FEEDS, FeedActionTypes, ALL, initFeeds } from "./feed" import { SourceGroupActionTypes, UPDATE_SOURCE_GROUP, ADD_SOURCE_TO_GROUP, DELETE_SOURCE_GROUP, REMOVE_SOURCE_FROM_GROUP } from "./group" import { PageActionTypes, SELECT_PAGE, PageType, selectAllArticles } from "./page" +import { getCurrentLocale, setLocaleSettings } from "../settings" +import locales from "../i18n/_locales" export enum ContextMenuType { Hidden, Item, Text, View @@ -28,6 +31,7 @@ export class AppLog { } export class AppState { + locale = null as string sourceInit = false feedInit = false fetchingItems = false @@ -149,12 +153,44 @@ export function exitSettings(): AppThunk { } } +export const INIT_INTL = "INIT_INTL" +export interface InitIntlAction { + type: typeof INIT_INTL + locale: string +} +export const initIntlDone = (locale: string): InitIntlAction => ({ + type: INIT_INTL, + locale: locale +}) + +export function initIntl(): AppThunk { + return (dispatch) => { + let locale = getCurrentLocale() + intl.init({ + currentLocale: locale, + locales: locales, + fallbackLocale: "zh-CN" + }).then(() => dispatch(initIntlDone(locale))) + } +} + +export function initApp(): AppThunk { + return (dispatch) => { + dispatch(initSources()).then(() => dispatch(initFeeds())).then(() => dispatch(fetchItems())) + dispatch(initIntl()) + } +} + export function appReducer( state = new AppState(), - action: SourceActionTypes | ItemActionTypes | ContextMenuActionTypes | SettingsActionTypes + action: SourceActionTypes | ItemActionTypes | ContextMenuActionTypes | SettingsActionTypes | InitIntlAction | MenuActionTypes | LogMenuActionType | FeedActionTypes | PageActionTypes | SourceGroupActionTypes ): AppState { switch (action.type) { + case INIT_INTL: return { + ...state, + locale: action.locale + } case INIT_SOURCES: switch (action.status) { case ActionStatus.Success: return { diff --git a/src/scripts/settings.ts b/src/scripts/settings.ts index 7e43e70..86b586f 100644 --- a/src/scripts/settings.ts +++ b/src/scripts/settings.ts @@ -1,6 +1,7 @@ import { remote, ipcRenderer } from "electron" import { ViewType } from "./models/page" import { IPartialTheme, loadTheme } from "@fluentui/react" +import locales from "./i18n/_locales" const PAC_STORE_KEY = "PAC" const PAC_STATUS_KEY = "PAC_ON" @@ -97,6 +98,20 @@ export function applyThemeSettings() { } } +const LOCALE_STORE_KEY = "locale" +export function setLocaleSettings(option: string) { + localStorage.setItem(LOCALE_STORE_KEY, option) +} +export function getLocaleSettings() { + let stored = localStorage.getItem(LOCALE_STORE_KEY) + return stored === null ? "default" : stored +} +export function getCurrentLocale() { + let set = getLocaleSettings() + let locale = set === "default" ? remote.app.getLocale() : set + return (locale in locales) ? locale : "en-US" +} + export const STORE_KEYS = [ PAC_STORE_KEY, PAC_STATUS_KEY, VIEW_STORE_KEY, THEME_STORE_KEY ] \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index e1f8555..adcb1ed 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,8 @@ { "compilerOptions": { "jsx": "react", + "resolveJsonModule": true, + "esModuleInterop": true, "target": "ES2019", "module": "CommonJS" }