mirror of
https://github.com/yang991178/fluent-reader.git
synced 2025-04-25 07:28:53 +02:00
add feedbin support
This commit is contained in:
parent
2676be240f
commit
acb53f3f8e
@ -3,6 +3,7 @@ import intl from "react-intl-universal"
|
|||||||
import { ServiceConfigs, SyncService } from "../../schema-types"
|
import { ServiceConfigs, SyncService } from "../../schema-types"
|
||||||
import { Stack, Icon, Link, Dropdown, IDropdownOption } from "@fluentui/react"
|
import { Stack, Icon, Link, Dropdown, IDropdownOption } from "@fluentui/react"
|
||||||
import FeverConfigsTab from "./services/fever"
|
import FeverConfigsTab from "./services/fever"
|
||||||
|
import FeedbinConfigsTab from "./services/feedbin"
|
||||||
|
|
||||||
type ServiceTabProps = {
|
type ServiceTabProps = {
|
||||||
configs: ServiceConfigs
|
configs: ServiceConfigs
|
||||||
@ -31,6 +32,7 @@ export class ServiceTab extends React.Component<ServiceTabProps, ServiceTabState
|
|||||||
|
|
||||||
serviceOptions = (): IDropdownOption[] => [
|
serviceOptions = (): IDropdownOption[] => [
|
||||||
{ key: SyncService.Fever, text: "Fever API" },
|
{ key: SyncService.Fever, text: "Fever API" },
|
||||||
|
{ key: SyncService.Feedbin, text: "Feedbin" },
|
||||||
{ key: -1, text: intl.get("service.suggest") },
|
{ key: -1, text: intl.get("service.suggest") },
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -46,6 +48,14 @@ export class ServiceTab extends React.Component<ServiceTabProps, ServiceTabState
|
|||||||
this.setState({ type: SyncService.None })
|
this.setState({ type: SyncService.None })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getConfigsTab = () => {
|
||||||
|
switch (this.state.type) {
|
||||||
|
case SyncService.Fever: return <FeverConfigsTab {...this.props} exit={this.exitConfigsTab} />
|
||||||
|
case SyncService.Feedbin: return <FeedbinConfigsTab {...this.props} exit={this.exitConfigsTab} />
|
||||||
|
default: return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render = () => (
|
render = () => (
|
||||||
<div className="tab-body">
|
<div className="tab-body">
|
||||||
{this.state.type === SyncService.None
|
{this.state.type === SyncService.None
|
||||||
@ -72,7 +82,7 @@ export class ServiceTab extends React.Component<ServiceTabProps, ServiceTabState
|
|||||||
style={{marginTop: 32, width: 180}} />
|
style={{marginTop: 32, width: 180}} />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
: <FeverConfigsTab {...this.props} exit={this.exitConfigsTab} />}
|
: this.getConfigsTab()}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
179
src/components/settings/services/feedbin.tsx
Normal file
179
src/components/settings/services/feedbin.tsx
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import intl from "react-intl-universal"
|
||||||
|
import { ServiceConfigsTabProps } from "../service"
|
||||||
|
import { FeedbinConfigs } from "../../../scripts/models/services/feedbin"
|
||||||
|
import { SyncService } from "../../../schema-types"
|
||||||
|
import { Stack, Icon, Label, TextField, PrimaryButton, DefaultButton, Checkbox,
|
||||||
|
MessageBar, MessageBarType, Dropdown, IDropdownOption } from "@fluentui/react"
|
||||||
|
import DangerButton from "../../utils/danger-button"
|
||||||
|
import { urlTest } from "../../../scripts/utils"
|
||||||
|
|
||||||
|
type FeedbinConfigsTabState = {
|
||||||
|
existing: boolean
|
||||||
|
endpoint: string
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
fetchLimit: number
|
||||||
|
importGroups: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
class FeedbinConfigsTab extends React.Component<ServiceConfigsTabProps, FeedbinConfigsTabState> {
|
||||||
|
constructor(props: ServiceConfigsTabProps) {
|
||||||
|
super(props)
|
||||||
|
const configs = props.configs as FeedbinConfigs
|
||||||
|
this.state = {
|
||||||
|
existing: configs.type === SyncService.Feedbin,
|
||||||
|
endpoint: configs.endpoint || "https://api.feedbin.me/v2/",
|
||||||
|
username: configs.username || "",
|
||||||
|
password: "",
|
||||||
|
fetchLimit: configs.fetchLimit || 250,
|
||||||
|
importGroups: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchLimitOptions = (): IDropdownOption[] => [
|
||||||
|
{ key: 250, text: intl.get("service.fetchLimitNum", { count: 250 }) },
|
||||||
|
{ key: 500, text: intl.get("service.fetchLimitNum", { count: 500 }) },
|
||||||
|
{ key: 750, text: intl.get("service.fetchLimitNum", { count: 750 }) },
|
||||||
|
{ key: 1000, text: intl.get("service.fetchLimitNum", { count: 1000 }) },
|
||||||
|
]
|
||||||
|
onFetchLimitOptionChange = (_, option: IDropdownOption) => {
|
||||||
|
this.setState({ fetchLimit: option.key as number })
|
||||||
|
}
|
||||||
|
|
||||||
|
handleInputChange = (event) => {
|
||||||
|
const name: string = event.target.name
|
||||||
|
// @ts-expect-error
|
||||||
|
this.setState({[name]: event.target.value})
|
||||||
|
}
|
||||||
|
|
||||||
|
checkNotEmpty = (v: string) => {
|
||||||
|
return (!this.state.existing && v.length == 0) ? intl.get("emptyField") : ""
|
||||||
|
}
|
||||||
|
|
||||||
|
validateForm = () => {
|
||||||
|
return urlTest(this.state.endpoint.trim()) && (this.state.existing || (this.state.username && this.state.password))
|
||||||
|
}
|
||||||
|
|
||||||
|
save = async () => {
|
||||||
|
let configs: FeedbinConfigs
|
||||||
|
if (this.state.existing) {
|
||||||
|
configs = {
|
||||||
|
...this.props.configs,
|
||||||
|
endpoint: this.state.endpoint,
|
||||||
|
fetchLimit: this.state.fetchLimit
|
||||||
|
} as FeedbinConfigs
|
||||||
|
if (this.state.password)
|
||||||
|
configs.password = this.state.password
|
||||||
|
} else {
|
||||||
|
configs = {
|
||||||
|
type: SyncService.Feedbin,
|
||||||
|
endpoint: this.state.endpoint,
|
||||||
|
username: this.state.username,
|
||||||
|
password: this.state.password,
|
||||||
|
fetchLimit: this.state.fetchLimit,
|
||||||
|
}
|
||||||
|
if (this.state.importGroups) configs.importGroups = true
|
||||||
|
}
|
||||||
|
this.props.blockActions()
|
||||||
|
const valid = await this.props.authenticate(configs)
|
||||||
|
if (valid) {
|
||||||
|
this.props.save(configs)
|
||||||
|
this.setState({ existing: true })
|
||||||
|
this.props.sync()
|
||||||
|
} else {
|
||||||
|
this.props.blockActions()
|
||||||
|
window.utils.showErrorBox(intl.get("service.failure"), intl.get("service.failureHint"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remove = async () => {
|
||||||
|
this.props.exit()
|
||||||
|
await this.props.remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <>
|
||||||
|
{!this.state.existing && (
|
||||||
|
<MessageBar messageBarType={MessageBarType.warning}>{intl.get("service.overwriteWarning")}</MessageBar>
|
||||||
|
)}
|
||||||
|
<Stack horizontalAlign="center" style={{marginTop: 48}}>
|
||||||
|
<svg style={{fill: "var(--black)", width: 32, userSelect: "none"}} viewBox="0 0 120 120"><path d="M116.4,87.2c-22.5-0.1-96.9-0.1-112.4,0c-4.9,0-4.8-22.5,0-23.3c15.6-2.5,60.3,0,60.3,0s16.1,16.3,20.8,16.3 c4.8,0,16.1-16.3,16.1-16.3s12.8-2.3,15.2,0C120.3,67.9,121.2,87.3,116.4,87.2z" /><path d="M110.9,108.8L110.9,108.8c-19.1,2.5-83.6,1.9-103,0c-4.3-0.4-1.5-13.6-1.5-13.6h108.1 C114.4,95.2,116.3,108.1,110.9,108.8z" /><path d="M58.1,9.9C30.6,6.2,7.9,29.1,7.9,51.3l102.6,1C110.6,30.2,85.4,13.6,58.1,9.9z" /></svg>
|
||||||
|
<Label style={{margin: "8px 0 36px"}}>Feedbin</Label>
|
||||||
|
<Stack className="login-form" horizontal>
|
||||||
|
<Stack.Item>
|
||||||
|
<Label>{intl.get("service.endpoint")}</Label>
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item grow>
|
||||||
|
<TextField
|
||||||
|
onGetErrorMessage={v => urlTest(v.trim()) ? "" : intl.get("sources.badUrl")}
|
||||||
|
validateOnLoad={false}
|
||||||
|
name="endpoint"
|
||||||
|
value={this.state.endpoint}
|
||||||
|
onChange={this.handleInputChange} />
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
<Stack className="login-form" horizontal>
|
||||||
|
<Stack.Item>
|
||||||
|
<Label>Email</Label>
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item grow>
|
||||||
|
<TextField
|
||||||
|
disabled={this.state.existing}
|
||||||
|
onGetErrorMessage={this.checkNotEmpty}
|
||||||
|
validateOnLoad={false}
|
||||||
|
name="username"
|
||||||
|
value={this.state.username}
|
||||||
|
onChange={this.handleInputChange} />
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
<Stack className="login-form" horizontal>
|
||||||
|
<Stack.Item>
|
||||||
|
<Label>{intl.get("service.password")}</Label>
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item grow>
|
||||||
|
<TextField
|
||||||
|
type="password"
|
||||||
|
placeholder={this.state.existing ? intl.get("service.unchanged") : ""}
|
||||||
|
onGetErrorMessage={this.checkNotEmpty}
|
||||||
|
validateOnLoad={false}
|
||||||
|
name="password"
|
||||||
|
value={this.state.password}
|
||||||
|
onChange={this.handleInputChange} />
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
<Stack className="login-form" horizontal>
|
||||||
|
<Stack.Item>
|
||||||
|
<Label>{intl.get("service.fetchLimit")}</Label>
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item grow>
|
||||||
|
<Dropdown
|
||||||
|
options={this.fetchLimitOptions()}
|
||||||
|
selectedKey={this.state.fetchLimit}
|
||||||
|
onChange={this.onFetchLimitOptionChange} />
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
{!this.state.existing && <Checkbox
|
||||||
|
label={intl.get("service.importGroups")}
|
||||||
|
checked={this.state.importGroups}
|
||||||
|
onChange={(_, c) => this.setState({importGroups: c})} />}
|
||||||
|
<Stack horizontal style={{marginTop: 32}}>
|
||||||
|
<Stack.Item>
|
||||||
|
<PrimaryButton
|
||||||
|
disabled={!this.validateForm()}
|
||||||
|
onClick={this.save}
|
||||||
|
text={this.state.existing ? intl.get("edit") : intl.get("confirm")} />
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item>
|
||||||
|
{this.state.existing
|
||||||
|
? <DangerButton onClick={this.remove} text={intl.get("delete")} />
|
||||||
|
: <DefaultButton onClick={this.props.exit} text={intl.get("cancel")} />
|
||||||
|
}
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FeedbinConfigsTab
|
@ -37,7 +37,7 @@ export const enum ImageCallbackTypes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const enum SyncService {
|
export const enum SyncService {
|
||||||
None, Fever
|
None, Fever, Feedbin
|
||||||
}
|
}
|
||||||
export interface ServiceConfigs {
|
export interface ServiceConfigs {
|
||||||
type: SyncService
|
type: SyncService
|
||||||
|
@ -248,15 +248,15 @@ export function markRead(item: RSSItem): AppThunk {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function markAllRead(sids: number[] = null, date: Date = null, before = true): AppThunk {
|
export function markAllRead(sids: number[] = null, date: Date = null, before = true): AppThunk<Promise<void>> {
|
||||||
return (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
let state = getState()
|
let state = getState()
|
||||||
if (sids === null) {
|
if (sids === null) {
|
||||||
let feed = state.feeds[state.page.feedId]
|
let feed = state.feeds[state.page.feedId]
|
||||||
sids = feed.sids
|
sids = feed.sids
|
||||||
}
|
}
|
||||||
const action = dispatch(getServiceHooks()).markAllRead?.(sids, date, before)
|
const action = dispatch(getServiceHooks()).markAllRead?.(sids, date, before)
|
||||||
if (action) dispatch(action)
|
if (action) await dispatch(action)
|
||||||
let query = {
|
let query = {
|
||||||
source: { $in: sids },
|
source: { $in: sids },
|
||||||
hasRead: false,
|
hasRead: false,
|
||||||
|
@ -2,13 +2,14 @@ import * as db from "../db"
|
|||||||
import { SyncService, ServiceConfigs } from "../../schema-types"
|
import { SyncService, ServiceConfigs } from "../../schema-types"
|
||||||
import { AppThunk, ActionStatus, fetchFavicon } from "../utils"
|
import { AppThunk, ActionStatus, fetchFavicon } from "../utils"
|
||||||
import { RSSItem, insertItems, fetchItemsSuccess } from "./item"
|
import { RSSItem, insertItems, fetchItemsSuccess } from "./item"
|
||||||
|
|
||||||
import { feverServiceHooks } from "./services/fever"
|
|
||||||
import { saveSettings, pushNotification } from "./app"
|
import { saveSettings, pushNotification } from "./app"
|
||||||
import { deleteSource, updateUnreadCounts, RSSSource, insertSource, addSourceSuccess, updateSource } from "./source"
|
import { deleteSource, updateUnreadCounts, RSSSource, insertSource, addSourceSuccess, updateSource } from "./source"
|
||||||
import { FilterType, initFeeds } from "./feed"
|
import { FilterType, initFeeds } from "./feed"
|
||||||
import { createSourceGroup, addSourceToGroup } from "./group"
|
import { createSourceGroup, addSourceToGroup } from "./group"
|
||||||
|
|
||||||
|
import { feverServiceHooks } from "./services/fever"
|
||||||
|
import { feedbinServiceHooks } from "./services/feedbin"
|
||||||
|
|
||||||
export interface ServiceHooks {
|
export interface ServiceHooks {
|
||||||
authenticate?: (configs: ServiceConfigs) => Promise<boolean>
|
authenticate?: (configs: ServiceConfigs) => Promise<boolean>
|
||||||
updateSources?: () => AppThunk<Promise<[RSSSource[], Map<number | string, string>]>>
|
updateSources?: () => AppThunk<Promise<[RSSSource[], Map<number | string, string>]>>
|
||||||
@ -16,7 +17,7 @@ export interface ServiceHooks {
|
|||||||
syncItems?: () => AppThunk<Promise<[(number | string)[], (number | string)[]]>>
|
syncItems?: () => AppThunk<Promise<[(number | string)[], (number | string)[]]>>
|
||||||
markRead?: (item: RSSItem) => AppThunk
|
markRead?: (item: RSSItem) => AppThunk
|
||||||
markUnread?: (item: RSSItem) => AppThunk
|
markUnread?: (item: RSSItem) => AppThunk
|
||||||
markAllRead?: (sids?: number[], date?: Date, before?: boolean) => AppThunk
|
markAllRead?: (sids?: number[], date?: Date, before?: boolean) => AppThunk<Promise<void>>
|
||||||
star?: (item: RSSItem) => AppThunk
|
star?: (item: RSSItem) => AppThunk
|
||||||
unstar?: (item: RSSItem) => AppThunk
|
unstar?: (item: RSSItem) => AppThunk
|
||||||
}
|
}
|
||||||
@ -24,6 +25,7 @@ export interface ServiceHooks {
|
|||||||
export function getServiceHooksFromType(type: SyncService): ServiceHooks {
|
export function getServiceHooksFromType(type: SyncService): ServiceHooks {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case SyncService.Fever: return feverServiceHooks
|
case SyncService.Fever: return feverServiceHooks
|
||||||
|
case SyncService.Feedbin: return feedbinServiceHooks
|
||||||
default: return {}
|
default: return {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -96,6 +98,7 @@ function updateSources(hook: ServiceHooks["updateSources"]): AppThunk<Promise<vo
|
|||||||
inserted.unreadCount = 0
|
inserted.unreadCount = 0
|
||||||
resolve(inserted)
|
resolve(inserted)
|
||||||
dispatch(addSourceSuccess(inserted, true))
|
dispatch(addSourceSuccess(inserted, true))
|
||||||
|
window.settings.saveGroups(getState().groups)
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
reject(err)
|
reject(err)
|
||||||
|
210
src/scripts/models/services/feedbin.ts
Normal file
210
src/scripts/models/services/feedbin.ts
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
import intl from "react-intl-universal"
|
||||||
|
import * as db from "../../db"
|
||||||
|
import { ServiceHooks } from "../service"
|
||||||
|
import { ServiceConfigs, SyncService } from "../../../schema-types"
|
||||||
|
import { createSourceGroup } from "../group"
|
||||||
|
import { RSSSource } from "../source"
|
||||||
|
import { htmlDecode, domParser } from "../../utils"
|
||||||
|
import { RSSItem } from "../item"
|
||||||
|
import { SourceRule } from "../rule"
|
||||||
|
|
||||||
|
export interface FeedbinConfigs extends ServiceConfigs {
|
||||||
|
type: SyncService.Feedbin
|
||||||
|
endpoint: string
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
fetchLimit: number
|
||||||
|
lastId?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchAPI(configs: FeedbinConfigs, params: string) {
|
||||||
|
const headers = new Headers()
|
||||||
|
headers.set("Authorization", "Basic " + btoa(configs.username + ":" + configs.password))
|
||||||
|
return await fetch(configs.endpoint + params, { headers: headers })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function markItems(configs: FeedbinConfigs, type: string, method: string, refs: number[]) {
|
||||||
|
const headers = new Headers()
|
||||||
|
headers.set("Authorization", "Basic " + btoa(configs.username + ":" + configs.password))
|
||||||
|
headers.set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
const promises = new Array<Promise<Response>>()
|
||||||
|
while (refs.length > 0) {
|
||||||
|
const batch = new Array<number>()
|
||||||
|
while (batch.length < 1000 && refs.length > 0) {
|
||||||
|
batch.push(refs.pop())
|
||||||
|
}
|
||||||
|
const bodyObject: any = {}
|
||||||
|
bodyObject[`${type}_entries`] = batch
|
||||||
|
promises.push(fetch(configs.endpoint + type + "_entries.json", {
|
||||||
|
method: method,
|
||||||
|
headers: headers,
|
||||||
|
body: JSON.stringify(bodyObject)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return await Promise.all(promises)
|
||||||
|
}
|
||||||
|
|
||||||
|
const APIError = () => new Error(intl.get("service.failure"))
|
||||||
|
|
||||||
|
export const feedbinServiceHooks: ServiceHooks = {
|
||||||
|
authenticate: async (configs: FeedbinConfigs) => {
|
||||||
|
try {
|
||||||
|
const result = await fetchAPI(configs, "authentication.json")
|
||||||
|
return result.status === 200
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateSources: () => async (dispatch, getState) => {
|
||||||
|
const configs = getState().service as FeedbinConfigs
|
||||||
|
const response = await fetchAPI(configs, "subscriptions.json")
|
||||||
|
if (response.status !== 200) throw APIError()
|
||||||
|
const subscriptions: any[] = await response.json()
|
||||||
|
let groupsMap: Map<number, string>
|
||||||
|
if (configs.importGroups) {
|
||||||
|
const tagsResponse = await fetchAPI(configs, "taggings.json")
|
||||||
|
if (tagsResponse.status !== 200) throw APIError()
|
||||||
|
const tags: any[] = await tagsResponse.json()
|
||||||
|
const tagsSet = new Set<string>()
|
||||||
|
groupsMap = new Map()
|
||||||
|
for (let tag of tags) {
|
||||||
|
const title = tag.name.trim()
|
||||||
|
if (!tagsSet.has(title)) {
|
||||||
|
tagsSet.add(title)
|
||||||
|
dispatch(createSourceGroup(title))
|
||||||
|
}
|
||||||
|
groupsMap.set(tag.feed_id, title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const sources = subscriptions.map(s => {
|
||||||
|
const source = new RSSSource(s.feed_url, s.title)
|
||||||
|
source.serviceRef = s.feed_id
|
||||||
|
return source
|
||||||
|
})
|
||||||
|
return [sources, groupsMap]
|
||||||
|
},
|
||||||
|
|
||||||
|
syncItems: () => async (_, getState) => {
|
||||||
|
const configs = getState().service as FeedbinConfigs
|
||||||
|
const [unreadResponse, starredResponse] = await Promise.all([
|
||||||
|
fetchAPI(configs, "unread_entries.json"),
|
||||||
|
fetchAPI(configs, "starred_entries.json")
|
||||||
|
])
|
||||||
|
if (unreadResponse.status !== 200 || starredResponse.status !== 200) throw APIError()
|
||||||
|
const unread = await unreadResponse.json()
|
||||||
|
const starred = await starredResponse.json()
|
||||||
|
return [unread, starred]
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchItems: () => async (_, getState) => {
|
||||||
|
const state = getState()
|
||||||
|
const configs = state.service as FeedbinConfigs
|
||||||
|
const items = new Array()
|
||||||
|
configs.lastId = configs.lastId || 0
|
||||||
|
let page = 1
|
||||||
|
let min = Number.MAX_SAFE_INTEGER
|
||||||
|
let lastFetched: any[]
|
||||||
|
do {
|
||||||
|
const response = await fetchAPI(configs, "entries.json?mode=extended&page=" + page)
|
||||||
|
if (response.status !== 200) throw APIError()
|
||||||
|
lastFetched = await response.json()
|
||||||
|
items.push(...lastFetched.filter(i => i.id > configs.lastId && i.id < min))
|
||||||
|
min = lastFetched.reduce((m, n) => Math.min(m, n.id), min)
|
||||||
|
page += 1
|
||||||
|
} while (
|
||||||
|
min > configs.lastId &&
|
||||||
|
lastFetched && lastFetched.length >= 50 &&
|
||||||
|
items.length < configs.fetchLimit
|
||||||
|
)
|
||||||
|
configs.lastId = items.reduce((m, n) => Math.max(m, n.id), configs.lastId)
|
||||||
|
if (items.length > 0) {
|
||||||
|
const fidMap = new Map<number, RSSSource>()
|
||||||
|
for (let source of Object.values(state.sources)) {
|
||||||
|
if (source.serviceRef) {
|
||||||
|
fidMap.set(source.serviceRef as number, source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const [unreadResponse, starredResponse] = await Promise.all([
|
||||||
|
fetchAPI(configs, "unread_entries.json"),
|
||||||
|
fetchAPI(configs, "starred_entries.json")
|
||||||
|
])
|
||||||
|
if (unreadResponse.status !== 200 || starredResponse.status !== 200) throw APIError()
|
||||||
|
const unread: Set<number> = new Set(await unreadResponse.json())
|
||||||
|
const starred: Set<number> = new Set(await starredResponse.json())
|
||||||
|
const parsedItems = items.map(i => {
|
||||||
|
const source = fidMap.get(i.feed_id)
|
||||||
|
const item = {
|
||||||
|
source: source.sid,
|
||||||
|
title: i.title,
|
||||||
|
link: i.url,
|
||||||
|
date: new Date(i.published),
|
||||||
|
fetchedDate: new Date(i.created_at),
|
||||||
|
content: i.content,
|
||||||
|
snippet: htmlDecode(i.content).trim(),
|
||||||
|
creator: i.author,
|
||||||
|
hasRead: !unread.has(i.id),
|
||||||
|
serviceRef: i.id,
|
||||||
|
} as RSSItem
|
||||||
|
if (starred.has(i.id)) item.starred = true
|
||||||
|
if (i.images && i.images.original_url) {
|
||||||
|
item.thumb = i.images.original_url
|
||||||
|
} else {
|
||||||
|
let dom = domParser.parseFromString(item.content, "text/html")
|
||||||
|
let baseEl = dom.createElement('base')
|
||||||
|
baseEl.setAttribute('href', item.link.split("/").slice(0, 3).join("/"))
|
||||||
|
dom.head.append(baseEl)
|
||||||
|
let img = dom.querySelector("img")
|
||||||
|
if (img && img.src) item.thumb = img.src
|
||||||
|
}
|
||||||
|
// Apply rules and sync back to the service
|
||||||
|
if (source.rules) SourceRule.applyAll(source.rules, item)
|
||||||
|
if (unread.has(i.id) === item.hasRead)
|
||||||
|
markItems(configs, "unread", item.hasRead ? "DELETE" : "POST", [i.id])
|
||||||
|
if (starred.has(i.id) !== Boolean(item.starred))
|
||||||
|
markItems(configs, "starred", item.starred ? "POST" : "DELETE", [i.id])
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
return [parsedItems, configs]
|
||||||
|
} else {
|
||||||
|
return [[], configs]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
markAllRead: (sids, date, before) => (_, getState) => new Promise(resolve => {
|
||||||
|
const state = getState()
|
||||||
|
const configs = state.service as FeedbinConfigs
|
||||||
|
const query: any = {
|
||||||
|
source: { $in: sids },
|
||||||
|
hasRead: false,
|
||||||
|
serviceRef: { $exists: true }
|
||||||
|
}
|
||||||
|
if (date) {
|
||||||
|
query.date = before ? { $lte: date } : { $gte: date }
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
db.idb.find(query, { serviceRef: 1 }, (err, docs) => {
|
||||||
|
resolve()
|
||||||
|
if (!err) {
|
||||||
|
const refs = docs.map(i => i.serviceRef as number)
|
||||||
|
markItems(configs, "unread", "DELETE", refs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
markRead: (item: RSSItem) => async (_, getState) => {
|
||||||
|
await markItems(getState().service as FeedbinConfigs, "unread", "DELETE", [item.serviceRef as number])
|
||||||
|
},
|
||||||
|
|
||||||
|
markUnread: (item: RSSItem) => async (_, getState) => {
|
||||||
|
await markItems(getState().service as FeedbinConfigs, "unread", "POST", [item.serviceRef as number])
|
||||||
|
},
|
||||||
|
|
||||||
|
star: (item: RSSItem) => async (_, getState) => {
|
||||||
|
await markItems(getState().service as FeedbinConfigs, "starred", "POST", [item.serviceRef as number])
|
||||||
|
},
|
||||||
|
|
||||||
|
unstar: (item: RSSItem) => async (_, getState) => {
|
||||||
|
await markItems(getState().service as FeedbinConfigs, "starred", "DELETE", [item.serviceRef as number])
|
||||||
|
},
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user