add partial intl

This commit is contained in:
刘浩远 2020-06-11 17:45:46 +08:00
parent ea75d5a893
commit 34b281f59a
16 changed files with 272 additions and 97 deletions

View File

@ -13,8 +13,8 @@ body {
body.dark {
color: #f8f8f8;
--gray: #a19f9d;
--primary: #3a96dd;
--primary-alt: #4ba0e1;
--primary: #4ba0e1;
--primary-alt: #65aee6;
}
a {

8
dist/styles.css vendored
View File

@ -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;

View File

@ -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",

View File

@ -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 && (
<div id="root"
key={locale}
onMouseDown={() => dispatch(closeContextMenu())}
onContextMenu={event => {
let text = document.getSelection().toString()
@ -24,4 +26,5 @@ const Root = ({ dispatch }) => (
</div>
)
export default connect()(Root)
const getLocale = (state: RootState) => ({ locale: state.app.locale })
export default connect(getLocale)(Root)

View File

@ -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<SettingsProps> {
<GroupsTabContainer />
</PivotItem>
<PivotItem headerText="应用选项" itemIcon="Settings">
<AppTab />
<AppTabContainer />
</PivotItem>
<PivotItem headerText="关于" itemIcon="Info">
<AboutTab />

View File

@ -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<AppTabProps> {
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 = () => (
<div className="tab-body">
<Label>{intl.get("app.language")}</Label>
<Stack horizontal>
<Stack.Item>
<Dropdown
defaultSelectedKey={getLocaleSettings()}
options={this.languageOptions()}
onChanged={option => this.props.setLanguage(String(option.key))}
style={{width: 200}} />
</Stack.Item>
</Stack>
<ChoiceGroup
label={intl.get("app.theme")}
options={this.themeChoices()}
onChange={this.onThemeChange}
selectedKey={this.state.themeSettings} />
<Stack horizontal verticalAlign="baseline">
<Stack.Item grow>
<Label></Label>
<Label>{intl.get("app.enableProxy")}</Label>
</Stack.Item>
<Stack.Item>
<Toggle checked={this.state.pacStatus} onChange={this.toggleStatus} />
@ -54,8 +82,8 @@ class AppTab extends React.Component {
<Stack.Item grow>
<TextField
required
onGetErrorMessage={v => 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 {
<DefaultButton
disabled={!urlTest(this.state.pacUrl)}
type="sumbit"
text="设置" />
text={intl.get("app.setPac")} />
</Stack.Item>
</Stack>
</form>}
<ChoiceGroup
label="应用主题"
options={themeChoices}
onChange={this.onThemeChange}
selectedKey={this.state.themeSettings} />
</div>
)
}

View File

@ -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 && (
<img src={s.iconurl} className="favicon" />
)
},
{
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<SourcesTabProps, SourcesTabState> {
selection: Selection
@ -78,6 +43,42 @@ class SourcesTab extends React.Component<SourcesTabProps, SourcesTabState> {
})
}
columns = (): IColumn[] => [
{
key: "favicon",
name: intl.get("icon"),
fieldName: "name",
isIconOnly: true,
iconName: "ImagePixel",
minWidth: 16,
maxWidth: 16,
onRender: (s: RSSSource) => s.iconurl && (
<img src={s.iconurl} className="favicon" />
)
},
{
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<SourcesTabProps, SourcesTabState> {
render = () => (
<div className="tab-body">
<Label>OPML文件</Label>
<Label>{intl.get("sources.opmlFile")}</Label>
<Stack horizontal>
<Stack.Item>
<PrimaryButton onClick={this.props.importOPML} text="导入文件" />
<PrimaryButton onClick={this.props.importOPML} text={intl.get("sources.import")} />
</Stack.Item>
<Stack.Item>
<DefaultButton text="导出文件" />
<DefaultButton text={intl.get("sources.export")} />
</Stack.Item>
</Stack>
<form onSubmit={this.addSource}>
<Label htmlFor="newUrl"></Label>
<Label htmlFor="newUrl">{intl.get("sources.add")}</Label>
<Stack horizontal>
<Stack.Item grow>
<TextField
onGetErrorMessage={v => 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<SourcesTabProps, SourcesTabState> {
<PrimaryButton
disabled={!urlTest(this.state.newUrl)}
type="submit"
text="添加" />
text={intl.get("add")} />
</Stack.Item>
</Stack>
</form>
<DetailsList
items={Object.values(this.props.sources)}
columns={columns}
columns={this.columns()}
getKey={s => s.sid}
setKey="selected"
selection={this.selection}
selectionMode={SelectionMode.single} />
{this.state.selectedSource && <>
<Label></Label>
<Label>{intl.get("sources.selected")}</Label>
<Stack horizontal>
<Stack.Item grow>
<TextField
onGetErrorMessage={v => 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<SourcesTabProps, SourcesTabState> {
<DefaultButton
disabled={this.state.newSourceName.length == 0}
onClick={() => this.props.updateSourceName(this.state.selectedSource, this.state.newSourceName)}
text="修改名称" />
text={intl.get("sources.editName")} />
</Stack.Item>
</Stack>
<ChoiceGroup
label="订阅源文章打开方式"
options={sourceOpenTargetChoices}
label={intl.get("sources.openTarget")}
options={this.sourceOpenTargetChoices()}
selectedKey={String(this.state.selectedSource.openTarget)}
onChange={this.onOpenTargetChange} />
<Stack horizontal style={{marginTop: 24}}>
<Stack horizontal>
<Stack.Item>
<DangerButton
onClick={() => this.props.deleteSource(this.state.selectedSource)}
key={this.state.selectedSource.sid}
text={`删除订阅源`} />
text={intl.get("sources.delete")} />
</Stack.Item>
<Stack.Item>
<span className="settings-hint"></span>
<span className="settings-hint">{intl.get("sources.deleteWarning")}</span>
</Stack.Item>
</Stack>
</>}

View File

@ -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

View File

@ -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"
}

View File

@ -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<AppDispatch, RootState>(thunkMiddleware)
)
store.dispatch(initSources()).then(() => store.dispatch(initFeeds())).then(() => store.dispatch(fetchItems()))
store.dispatch(initApp())
ReactDOM.render(
<Provider store={store}>

View File

@ -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

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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 {

View File

@ -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
]

View File

@ -1,6 +1,8 @@
{
"compilerOptions": {
"jsx": "react",
"resolveJsonModule": true,
"esModuleInterop": true,
"target": "ES2019",
"module": "CommonJS"
}