mirror of
https://github.com/yang991178/fluent-reader.git
synced 2025-02-09 08:18:38 +01:00
add service hooks
This commit is contained in:
parent
440fafc1a9
commit
3a8f13f5d4
@ -1,4 +1,4 @@
|
||||
import { SourceGroup, ViewType, ThemeSettings, SearchEngines } from "../schema-types"
|
||||
import { SourceGroup, ViewType, ThemeSettings, SearchEngines, ServiceConfigs } from "../schema-types"
|
||||
import { ipcRenderer } from "electron"
|
||||
|
||||
const settingsBridge = {
|
||||
@ -82,6 +82,13 @@ const settingsBridge = {
|
||||
ipcRenderer.invoke("set-search-engine", engine)
|
||||
},
|
||||
|
||||
getServiceConfigs: (): ServiceConfigs => {
|
||||
return ipcRenderer.sendSync("get-service-configs")
|
||||
},
|
||||
setServiceConfigs: (configs: ServiceConfigs) => {
|
||||
ipcRenderer.invoke("set-service-configs", configs)
|
||||
},
|
||||
|
||||
getAll: () => {
|
||||
return ipcRenderer.sendSync("get-all-settings") as Object
|
||||
},
|
||||
|
@ -8,6 +8,7 @@ import SourcesTabContainer from "../containers/settings/sources-container"
|
||||
import GroupsTabContainer from "../containers/settings/groups-container"
|
||||
import AppTabContainer from "../containers/settings/app-container"
|
||||
import RulesTabContainer from "../containers/settings/rules-container"
|
||||
import ServiceTabContainer from "../containers/settings/service-container"
|
||||
|
||||
type SettingsProps = {
|
||||
display: boolean,
|
||||
@ -42,6 +43,9 @@ class Settings extends React.Component<SettingsProps> {
|
||||
<PivotItem headerText={intl.get("settings.rules")} itemIcon="FilterSettings">
|
||||
<RulesTabContainer />
|
||||
</PivotItem>
|
||||
<PivotItem headerText={intl.get("settings.service")} itemIcon="CloudImportExport">
|
||||
<ServiceTabContainer />
|
||||
</PivotItem>
|
||||
<PivotItem headerText={intl.get("settings.app")} itemIcon="Settings">
|
||||
<AppTabContainer />
|
||||
</PivotItem>
|
||||
|
@ -3,7 +3,7 @@ import intl from "react-intl-universal"
|
||||
import { SourceGroup } from "../../schema-types"
|
||||
import { SourceState, RSSSource } from "../../scripts/models/source"
|
||||
import { IColumn, Selection, SelectionMode, DetailsList, Label, Stack,
|
||||
TextField, PrimaryButton, DefaultButton, Dropdown, IDropdownOption, CommandBarButton, MarqueeSelection, IDragDropEvents, IDragDropContext } from "@fluentui/react"
|
||||
TextField, PrimaryButton, DefaultButton, Dropdown, IDropdownOption, CommandBarButton, MarqueeSelection, IDragDropEvents } from "@fluentui/react"
|
||||
import DangerButton from "../utils/danger-button"
|
||||
|
||||
type GroupsTabProps = {
|
||||
@ -233,7 +233,7 @@ class GroupsTab extends React.Component<GroupsTabProps, GroupsTabState> {
|
||||
createGroup = (event: React.FormEvent) => {
|
||||
event.preventDefault()
|
||||
let trimmed = this.state.newGroupName.trim()
|
||||
if (trimmed.length > 0) this.props.createGroup(trimmed)
|
||||
if (this.validateNewGroupName(trimmed) === "") this.props.createGroup(trimmed)
|
||||
}
|
||||
|
||||
addToGroup = () => {
|
||||
|
64
src/components/settings/service.tsx
Normal file
64
src/components/settings/service.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import * as React from "react"
|
||||
import intl from "react-intl-universal"
|
||||
import { ServiceConfigs, SyncService } from "../../schema-types"
|
||||
import { Stack, Icon, Link, Dropdown, IDropdownOption } from "@fluentui/react"
|
||||
|
||||
export type ServiceTabProps = {
|
||||
configs: ServiceConfigs
|
||||
}
|
||||
|
||||
type ServiceTabState = {
|
||||
type: SyncService
|
||||
}
|
||||
|
||||
export class ServiceTab extends React.Component<ServiceTabProps, ServiceTabState> {
|
||||
constructor(props: ServiceTabProps) {
|
||||
super(props)
|
||||
this.state = {
|
||||
type: props.configs.type
|
||||
}
|
||||
}
|
||||
|
||||
serviceOptions = (): IDropdownOption[] => [
|
||||
{ key: SyncService.Fever, text: "Fever API" },
|
||||
{ key: -1, text: intl.get("service.suggest") },
|
||||
]
|
||||
|
||||
onServiceOptionChange = (_, option: IDropdownOption) => {
|
||||
if (option.key === -1) {
|
||||
window.utils.openExternal("https://github.com/yang991178/fluent-reader/issues/23")
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
render = () => (
|
||||
<div className="tab-body">
|
||||
{this.state.type === SyncService.None
|
||||
? (
|
||||
<Stack horizontalAlign="center" style={{marginTop: 64}}>
|
||||
<Stack className="settings-rules-icons" horizontal tokens={{childrenGap: 12}}>
|
||||
<Icon iconName="ThisPC" />
|
||||
<Icon iconName="Sync" />
|
||||
<Icon iconName="Cloud" />
|
||||
</Stack>
|
||||
<span className="settings-hint">
|
||||
{intl.get("service.intro")}
|
||||
<Link
|
||||
onClick={() => window.utils.openExternal("https://github.com/yang991178/fluent-reader/wiki/Support#services")}
|
||||
style={{marginLeft: 6}}>
|
||||
{intl.get("rules.help")}
|
||||
</Link>
|
||||
</span>
|
||||
<Dropdown
|
||||
placeHolder={intl.get("service.select")}
|
||||
options={this.serviceOptions()}
|
||||
selectedKey={null}
|
||||
onChange={this.onServiceOptionChange}
|
||||
style={{marginTop: 32, width: 180}} />
|
||||
</Stack>
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
)
|
||||
}
|
21
src/containers/settings/service-container.tsx
Normal file
21
src/containers/settings/service-container.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { connect } from "react-redux"
|
||||
import { createSelector } from "reselect"
|
||||
import { RootState } from "../../scripts/reducer"
|
||||
import { ServiceTab } from "../../components/settings/service"
|
||||
import { AppDispatch } from "../../scripts/utils"
|
||||
|
||||
const getService = (state: RootState) => state.service
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
[getService],
|
||||
(service) => ({
|
||||
configs: service
|
||||
})
|
||||
)
|
||||
|
||||
const mapDispatchToProps = (dispatch: AppDispatch) => ({
|
||||
|
||||
})
|
||||
|
||||
const ServiceTabContainer = connect(mapStateToProps, mapDispatchToProps)(ServiceTab)
|
||||
export default ServiceTabContainer
|
@ -1,5 +1,6 @@
|
||||
import Store = require("electron-store")
|
||||
import { SchemaTypes, SourceGroup, ViewType, ThemeSettings, SearchEngines } from "../schema-types"
|
||||
import { SchemaTypes, SourceGroup, ViewType, ThemeSettings, SearchEngines,
|
||||
SyncService, ServiceConfigs } from "../schema-types"
|
||||
import { ipcMain, session, nativeTheme, app } from "electron"
|
||||
import { WindowManager } from "./window"
|
||||
|
||||
@ -136,3 +137,11 @@ ipcMain.on("get-search-engine", (event) => {
|
||||
ipcMain.handle("set-search-engine", (_, engine: SearchEngines) => {
|
||||
store.set(SEARCH_ENGINE_STORE_KEY, engine)
|
||||
})
|
||||
|
||||
const SERVICE_CONFIGS_STORE_KEY = "serviceConfigs"
|
||||
ipcMain.on("get-service-configs", (event) => {
|
||||
event.returnValue = store.get(SERVICE_CONFIGS_STORE_KEY, { type: SyncService.None })
|
||||
})
|
||||
ipcMain.handle("set-service-configs", (_, configs: ServiceConfigs) => {
|
||||
store.set(SERVICE_CONFIGS_STORE_KEY, configs)
|
||||
})
|
||||
|
@ -36,6 +36,13 @@ export const enum ImageCallbackTypes {
|
||||
OpenExternal, OpenExternalBg, SaveAs, Copy, CopyLink
|
||||
}
|
||||
|
||||
export const enum SyncService {
|
||||
None, Fever
|
||||
}
|
||||
export interface ServiceConfigs {
|
||||
type: SyncService
|
||||
}
|
||||
|
||||
export type SchemaTypes = {
|
||||
version: string
|
||||
theme: ThemeSettings
|
||||
@ -48,4 +55,5 @@ export type SchemaTypes = {
|
||||
menuOn: boolean
|
||||
fetchInterval: number
|
||||
searchEngine: SearchEngines
|
||||
serviceConfigs: ServiceConfigs
|
||||
}
|
||||
|
@ -103,6 +103,7 @@
|
||||
"sources": "Sources",
|
||||
"grouping": "Groups",
|
||||
"rules": "Rules",
|
||||
"service": "Service",
|
||||
"app": "Preferences",
|
||||
"about": "About",
|
||||
"version": "Version",
|
||||
@ -175,6 +176,11 @@
|
||||
"hint": "Rules will be applied in order. Drag and drop to reorder.",
|
||||
"test": "Test rules"
|
||||
},
|
||||
"service": {
|
||||
"intro": "Sync across devices with RSS services.",
|
||||
"select": "Select a service",
|
||||
"suggest": "Suggest a new service"
|
||||
},
|
||||
"app": {
|
||||
"cleanup": "Clean up",
|
||||
"cache": "Clear cache",
|
||||
|
@ -101,6 +101,7 @@
|
||||
"sources": "订阅源",
|
||||
"grouping": "分组与排序",
|
||||
"rules": "规则",
|
||||
"service": "服务",
|
||||
"app": "应用偏好",
|
||||
"about": "关于",
|
||||
"version": "版本",
|
||||
@ -173,6 +174,11 @@
|
||||
"hint": "规则将按顺序执行,拖拽以排序",
|
||||
"test": "测试规则"
|
||||
},
|
||||
"service": {
|
||||
"intro": "通过 RSS 服务跨设备保持同步",
|
||||
"select": "选择服务",
|
||||
"suggest": "建议一项新服务"
|
||||
},
|
||||
"app": {
|
||||
"cleanup": "清理",
|
||||
"cache": "清空缓存",
|
||||
|
@ -5,6 +5,7 @@ import { RSSSource } from "./source"
|
||||
import { FeedActionTypes, INIT_FEED, LOAD_MORE, FilterType, initFeeds } from "./feed"
|
||||
import Parser from "@yang991178/rss-parser"
|
||||
import { pushNotification, setupAutoFetch } from "./app"
|
||||
import { getServiceHooks, syncWithService } from "./service"
|
||||
|
||||
export class RSSItem {
|
||||
_id: string
|
||||
@ -171,13 +172,18 @@ export function insertItems(items: RSSItem[]): Promise<RSSItem[]> {
|
||||
}
|
||||
|
||||
export function fetchItems(background = false): AppThunk<Promise<void>> {
|
||||
return (dispatch, getState) => {
|
||||
return async (dispatch, getState) => {
|
||||
try {
|
||||
await dispatch(syncWithService())
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
let promises = new Array<Promise<RSSItem[]>>()
|
||||
if (!getState().app.fetchingItems) {
|
||||
let timenow = new Date().getTime()
|
||||
let sources = <RSSSource[]>Object.values(getState().sources).filter(s => {
|
||||
let last = s.lastFetched ? s.lastFetched.getTime() : 0
|
||||
return (last > timenow) || (last + (s.fetchFrequency || 0) * 60000 <= timenow)
|
||||
return !s.isRemote && ((last > timenow) || (last + (s.fetchFrequency || 0) * 60000 <= timenow))
|
||||
})
|
||||
for (let source of sources) {
|
||||
let promise = RSSSource.fetchItems(source)
|
||||
@ -185,7 +191,8 @@ export function fetchItems(background = false): AppThunk<Promise<void>> {
|
||||
promises.push(promise)
|
||||
}
|
||||
dispatch(fetchItemsRequest(promises.length))
|
||||
return Promise.allSettled(promises).then(results => new Promise<void>((resolve, reject) => {
|
||||
const results = await Promise.allSettled(promises)
|
||||
return await new Promise<void>((resolve, reject) => {
|
||||
let items = new Array<RSSItem>()
|
||||
results.map((r, i) => {
|
||||
if (r.status === "fulfilled") items.push(...r.value)
|
||||
@ -216,9 +223,8 @@ export function fetchItems(background = false): AppThunk<Promise<void>> {
|
||||
console.log(err)
|
||||
reject(err)
|
||||
})
|
||||
}))
|
||||
})
|
||||
}
|
||||
return new Promise((resolve) => { resolve() })
|
||||
}
|
||||
}
|
||||
|
||||
@ -233,10 +239,13 @@ const markUnreadDone = (item: RSSItem): ItemActionTypes => ({
|
||||
})
|
||||
|
||||
export function markRead(item: RSSItem): AppThunk {
|
||||
return (dispatch) => {
|
||||
return (dispatch, getState) => {
|
||||
if (!item.hasRead) {
|
||||
db.idb.update({ _id: item._id }, { $set: { hasRead: true } })
|
||||
dispatch(markReadDone(item))
|
||||
if (getState().sources[item.source].isRemote) {
|
||||
dispatch(getServiceHooks()).markRead?.(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -287,14 +296,18 @@ export function markAllRead(sids: number[] = null, date: Date = null, before = t
|
||||
if (date) callback(affectedDocuments as unknown as RSSItem[])
|
||||
})
|
||||
if (!date) callback()
|
||||
dispatch(getServiceHooks()).markAllRead?.(sids, date, before)
|
||||
}
|
||||
}
|
||||
|
||||
export function markUnread(item: RSSItem): AppThunk {
|
||||
return (dispatch) => {
|
||||
return (dispatch, getState) => {
|
||||
if (item.hasRead) {
|
||||
db.idb.update({ _id: item._id }, { $set: { hasRead: false } })
|
||||
dispatch(markUnreadDone(item))
|
||||
if (getState().sources[item.source].isRemote) {
|
||||
dispatch(getServiceHooks()).markUnread?.(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -305,13 +318,18 @@ const toggleStarredDone = (item: RSSItem): ItemActionTypes => ({
|
||||
})
|
||||
|
||||
export function toggleStarred(item: RSSItem): AppThunk {
|
||||
return (dispatch) => {
|
||||
return (dispatch, getState) => {
|
||||
if (item.starred === true) {
|
||||
db.idb.update({ _id: item._id }, { $unset: { starred: true } })
|
||||
} else {
|
||||
db.idb.update({ _id: item._id }, { $set: { starred: true } })
|
||||
}
|
||||
dispatch(toggleStarredDone(item))
|
||||
if (getState().sources[item.source].isRemote) {
|
||||
const hooks = dispatch(getServiceHooks())
|
||||
if (item.starred) hooks.unstar?.(item)
|
||||
else hooks.star?.(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
70
src/scripts/models/service.ts
Normal file
70
src/scripts/models/service.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import { SyncService, ServiceConfigs } from "../../schema-types"
|
||||
import { AppThunk } from "../utils"
|
||||
import { RSSItem } from "./item"
|
||||
|
||||
import { feverServiceHooks } from "./services/fever"
|
||||
|
||||
export interface ServiceHooks {
|
||||
authenticate?: (configs: ServiceConfigs) => Promise<boolean>
|
||||
updateSources?: () => AppThunk<Promise<void>>
|
||||
fetchItems?: () => AppThunk<Promise<void>>
|
||||
syncItems?: () => AppThunk<Promise<void>>
|
||||
markRead?: (item: RSSItem) => AppThunk
|
||||
markUnread?: (item: RSSItem) => AppThunk
|
||||
markAllRead?: (sids?: number[], date?: Date, before?: boolean) => AppThunk
|
||||
star?: (item: RSSItem) => AppThunk
|
||||
unstar?: (item: RSSItem) => AppThunk
|
||||
}
|
||||
|
||||
export function getServiceHooksFromType(type: SyncService): ServiceHooks {
|
||||
switch (type) {
|
||||
case SyncService.Fever: return feverServiceHooks
|
||||
default: return {}
|
||||
}
|
||||
}
|
||||
|
||||
export function getServiceHooks(): AppThunk<ServiceHooks> {
|
||||
return (_, getState) => {
|
||||
return getServiceHooksFromType(getState().service.type)
|
||||
}
|
||||
}
|
||||
|
||||
export function syncWithService(): AppThunk<Promise<void>> {
|
||||
return async (dispatch) => {
|
||||
const hooks = dispatch(getServiceHooks())
|
||||
if (hooks.updateSources && hooks.fetchItems && hooks.syncItems) {
|
||||
await dispatch(hooks.updateSources())
|
||||
await dispatch(hooks.fetchItems())
|
||||
await dispatch(hooks.syncItems())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const SAVE_SERVICE_CONFIGS = "SAVE_SERVICE_CONFIGS"
|
||||
|
||||
interface SaveServiceConfigsAction {
|
||||
type: typeof SAVE_SERVICE_CONFIGS
|
||||
configs: ServiceConfigs
|
||||
}
|
||||
|
||||
export type ServiceActionTypes = SaveServiceConfigsAction
|
||||
|
||||
export function saveServiceConfigs(configs: ServiceConfigs): AppThunk {
|
||||
return (dispatch) => {
|
||||
window.settings.setServiceConfigs(configs)
|
||||
dispatch({
|
||||
type: SAVE_SERVICE_CONFIGS,
|
||||
configs: configs
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function serviceReducer(
|
||||
state = window.settings.getServiceConfigs(),
|
||||
action: ServiceActionTypes
|
||||
): ServiceConfigs {
|
||||
switch (action.type) {
|
||||
case SAVE_SERVICE_CONFIGS: return action.configs
|
||||
default: return state
|
||||
}
|
||||
}
|
29
src/scripts/models/services/fever.ts
Normal file
29
src/scripts/models/services/fever.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { ServiceHooks } from "../service"
|
||||
import { ServiceConfigs } from "../../../schema-types"
|
||||
|
||||
export interface FeverConfigs extends ServiceConfigs {
|
||||
endpoint: string
|
||||
username: string
|
||||
password: string
|
||||
apiKey: string
|
||||
lastId?: number
|
||||
}
|
||||
|
||||
async function fetchAPI(configs: FeverConfigs, params="") {
|
||||
const response = await fetch(configs.endpoint + params, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/x-www-form-urlencoded" },
|
||||
body: `api_key=${configs.apiKey}`
|
||||
})
|
||||
return await response.json()
|
||||
}
|
||||
|
||||
export const feverServiceHooks: ServiceHooks = {
|
||||
authenticate: async (configs: FeverConfigs) => {
|
||||
try {
|
||||
return Boolean((await fetchAPI(configs)).auth)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@ export class RSSSource {
|
||||
openTarget: SourceOpenTarget
|
||||
unreadCount: number
|
||||
lastFetched: Date
|
||||
isRemote?: true
|
||||
fetchFrequency?: number // in minutes
|
||||
rules?: SourceRule[]
|
||||
|
||||
|
@ -6,6 +6,7 @@ import { feedReducer } from "./models/feed"
|
||||
import { appReducer } from "./models/app"
|
||||
import { groupReducer } from "./models/group"
|
||||
import { pageReducer } from "./models/page"
|
||||
import { serviceReducer } from "./models/service"
|
||||
|
||||
export const rootReducer = combineReducers({
|
||||
sources: sourceReducer,
|
||||
@ -13,6 +14,7 @@ export const rootReducer = combineReducers({
|
||||
feeds: feedReducer,
|
||||
groups: groupReducer,
|
||||
page: pageReducer,
|
||||
service: serviceReducer,
|
||||
app: appReducer
|
||||
})
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user