mirror of
https://github.com/yang991178/fluent-reader.git
synced 2025-02-07 23:38:41 +01:00
added miniflux API service, accompanying settings component
This commit is contained in:
parent
b827f268ac
commit
5786561dc3
2
package-lock.json
generated
2
package-lock.json
generated
@ -6,7 +6,7 @@
|
|||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "fluent-reader",
|
"name": "fluent-reader",
|
||||||
"version": "1.1.1",
|
"version": "1.1.2",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@fluentui/react": "^7.126.2",
|
"@fluentui/react": "^7.126.2",
|
||||||
|
@ -6,6 +6,7 @@ import FeverConfigsTab from "./services/fever"
|
|||||||
import FeedbinConfigsTab from "./services/feedbin"
|
import FeedbinConfigsTab from "./services/feedbin"
|
||||||
import GReaderConfigsTab from "./services/greader"
|
import GReaderConfigsTab from "./services/greader"
|
||||||
import InoreaderConfigsTab from "./services/inoreader"
|
import InoreaderConfigsTab from "./services/inoreader"
|
||||||
|
import MinifluxConfigsTab from "./services/miniflux"
|
||||||
|
|
||||||
type ServiceTabProps = {
|
type ServiceTabProps = {
|
||||||
configs: ServiceConfigs
|
configs: ServiceConfigs
|
||||||
@ -41,6 +42,7 @@ export class ServiceTab extends React.Component<
|
|||||||
{ key: SyncService.Feedbin, text: "Feedbin" },
|
{ key: SyncService.Feedbin, text: "Feedbin" },
|
||||||
{ key: SyncService.GReader, text: "Google Reader API (Beta)" },
|
{ key: SyncService.GReader, text: "Google Reader API (Beta)" },
|
||||||
{ key: SyncService.Inoreader, text: "Inoreader" },
|
{ key: SyncService.Inoreader, text: "Inoreader" },
|
||||||
|
{ key: SyncService.Miniflux, text: "Miniflux" },
|
||||||
{ key: -1, text: intl.get("service.suggest") },
|
{ key: -1, text: intl.get("service.suggest") },
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -88,6 +90,13 @@ export class ServiceTab extends React.Component<
|
|||||||
exit={this.exitConfigsTab}
|
exit={this.exitConfigsTab}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
case SyncService.Miniflux:
|
||||||
|
return (
|
||||||
|
<MinifluxConfigsTab
|
||||||
|
{...this.props}
|
||||||
|
exit={this.exitConfigsTab}
|
||||||
|
/>
|
||||||
|
)
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
307
src/components/settings/services/miniflux.tsx
Normal file
307
src/components/settings/services/miniflux.tsx
Normal file
@ -0,0 +1,307 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import intl from "react-intl-universal"
|
||||||
|
import { ServiceConfigsTabProps } from "../service"
|
||||||
|
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"
|
||||||
|
import LiteExporter from "./lite-exporter"
|
||||||
|
import { MinifluxConfigs } from "../../../scripts/models/services/miniflux"
|
||||||
|
|
||||||
|
type MinifluxConfigsTabState = {
|
||||||
|
existing: boolean
|
||||||
|
endpoint: string
|
||||||
|
apiKeyAuth: boolean
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
apiKey: string
|
||||||
|
fetchLimit: number
|
||||||
|
importGroups: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
class MinifluxConfigsTab extends React.Component<
|
||||||
|
ServiceConfigsTabProps,
|
||||||
|
MinifluxConfigsTabState
|
||||||
|
> {
|
||||||
|
constructor(props: ServiceConfigsTabProps) {
|
||||||
|
super(props)
|
||||||
|
const configs = props.configs as MinifluxConfigs
|
||||||
|
this.state = {
|
||||||
|
existing: configs.type === SyncService.Miniflux,
|
||||||
|
endpoint: configs.endpoint || "",
|
||||||
|
apiKeyAuth: true,
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
apiKey: "",
|
||||||
|
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 }) },
|
||||||
|
{ key: 1500, text: intl.get("service.fetchLimitNum", { count: 1500 }) },
|
||||||
|
{
|
||||||
|
key: Number.MAX_SAFE_INTEGER,
|
||||||
|
text: intl.get("service.fetchUnlimited"),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
onFetchLimitOptionChange = (_, option: IDropdownOption) => {
|
||||||
|
this.setState({ fetchLimit: option.key as number })
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticationOptions = (): IDropdownOption[] => [
|
||||||
|
{ key: "apiKey", text: "API Key" /*intl.get("service.password")*/ },
|
||||||
|
{ key: "userPass", text: intl.get("service.username") + "/" + intl.get("service.password")}
|
||||||
|
]
|
||||||
|
onAuthenticationOptionsChange = (_, option: IDropdownOption) => {
|
||||||
|
this.setState({ apiKeyAuth: option.key == "apiKey" })
|
||||||
|
}
|
||||||
|
|
||||||
|
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.apiKey ||
|
||||||
|
(this.state.username && this.state.password))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
save = async () => {
|
||||||
|
let configs: MinifluxConfigs
|
||||||
|
|
||||||
|
if (this.state.existing)
|
||||||
|
{
|
||||||
|
configs = {
|
||||||
|
...this.props.configs,
|
||||||
|
endpoint: this.state.endpoint,
|
||||||
|
fetchLimit: this.state.fetchLimit,
|
||||||
|
} as MinifluxConfigs
|
||||||
|
|
||||||
|
if (this.state.apiKey || this.state.password) configs.authKey = this.state.apiKeyAuth ? this.state.apiKey :
|
||||||
|
Buffer.from(this.state.username + ":" + this.state.password, 'binary').toString('base64')
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
configs = {
|
||||||
|
type: SyncService.Miniflux,
|
||||||
|
endpoint: this.state.endpoint,
|
||||||
|
apiKeyAuth: this.state.apiKeyAuth,
|
||||||
|
authKey: this.state.apiKeyAuth ? this.state.apiKey :
|
||||||
|
Buffer.from(this.state.username + ":" + this.state.password, 'binary').toString('base64'),
|
||||||
|
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 }}>
|
||||||
|
<Icon
|
||||||
|
iconName="MarkDownLanguage"
|
||||||
|
style={{
|
||||||
|
color: "var(--black)",
|
||||||
|
fontSize: 32,
|
||||||
|
userSelect: "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Label style={{ margin: "8px 0 36px" }}>
|
||||||
|
Miniflux
|
||||||
|
</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>{intl.get("groups.type")}</Label>
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item grow>
|
||||||
|
<Dropdown
|
||||||
|
options={this.authenticationOptions()}
|
||||||
|
selectedKey={this.state.apiKeyAuth ? "apiKey" : "userPass" }
|
||||||
|
onChange={this.onAuthenticationOptionsChange}
|
||||||
|
/>
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
{ this.state.apiKeyAuth && <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="apiKey"
|
||||||
|
value={this.state.apiKey}
|
||||||
|
onChange={this.handleInputChange}
|
||||||
|
/>
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack> }
|
||||||
|
{ !this.state.apiKeyAuth && <Stack className="login-form" horizontal>
|
||||||
|
<Stack.Item>
|
||||||
|
<Label>{intl.get("service.username")}</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> }
|
||||||
|
{ !this.state.apiKeyAuth && <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>
|
||||||
|
{this.state.existing && (
|
||||||
|
<LiteExporter serviceConfigs={this.props.configs} />
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MinifluxConfigsTab
|
@ -59,6 +59,7 @@ export const enum SyncService {
|
|||||||
Feedbin,
|
Feedbin,
|
||||||
GReader,
|
GReader,
|
||||||
Inoreader,
|
Inoreader,
|
||||||
|
Miniflux,
|
||||||
}
|
}
|
||||||
export interface ServiceConfigs {
|
export interface ServiceConfigs {
|
||||||
type: SyncService
|
type: SyncService
|
||||||
|
@ -18,6 +18,7 @@ import { createSourceGroup, addSourceToGroup } from "./group"
|
|||||||
import { feverServiceHooks } from "./services/fever"
|
import { feverServiceHooks } from "./services/fever"
|
||||||
import { feedbinServiceHooks } from "./services/feedbin"
|
import { feedbinServiceHooks } from "./services/feedbin"
|
||||||
import { gReaderServiceHooks } from "./services/greader"
|
import { gReaderServiceHooks } from "./services/greader"
|
||||||
|
import { minifluxServiceHooks} from "./services/miniflux"
|
||||||
|
|
||||||
export interface ServiceHooks {
|
export interface ServiceHooks {
|
||||||
authenticate?: (configs: ServiceConfigs) => Promise<boolean>
|
authenticate?: (configs: ServiceConfigs) => Promise<boolean>
|
||||||
@ -45,6 +46,8 @@ export function getServiceHooksFromType(type: SyncService): ServiceHooks {
|
|||||||
case SyncService.GReader:
|
case SyncService.GReader:
|
||||||
case SyncService.Inoreader:
|
case SyncService.Inoreader:
|
||||||
return gReaderServiceHooks
|
return gReaderServiceHooks
|
||||||
|
case SyncService.Miniflux:
|
||||||
|
return minifluxServiceHooks
|
||||||
default:
|
default:
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
321
src/scripts/models/services/miniflux.ts
Normal file
321
src/scripts/models/services/miniflux.ts
Normal file
@ -0,0 +1,321 @@
|
|||||||
|
import intl from "react-intl-universal"
|
||||||
|
import { ServiceHooks } from "../service"
|
||||||
|
import { ServiceConfigs, SyncService } from "../../../schema-types"
|
||||||
|
import { createSourceGroup } from "../group"
|
||||||
|
import { RSSSource } from "../source"
|
||||||
|
import { domParser, htmlDecode } from "../../utils"
|
||||||
|
import { RSSItem } from "../item"
|
||||||
|
import { SourceRule } from "../rule"
|
||||||
|
|
||||||
|
// miniflux service configs
|
||||||
|
export interface MinifluxConfigs extends ServiceConfigs {
|
||||||
|
type: SyncService.Miniflux
|
||||||
|
endpoint: string
|
||||||
|
apiKeyAuth: boolean
|
||||||
|
authKey: string
|
||||||
|
fetchLimit: number
|
||||||
|
lastId?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// partial api schema
|
||||||
|
interface Feed {
|
||||||
|
id: number
|
||||||
|
feed_url: string
|
||||||
|
title: string
|
||||||
|
category: { title: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Category {
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Entry {
|
||||||
|
id: number
|
||||||
|
status: "unread" | "read" | "removed"
|
||||||
|
title: string
|
||||||
|
url: string
|
||||||
|
published_at: string
|
||||||
|
created_at: string
|
||||||
|
content: string
|
||||||
|
author: string
|
||||||
|
starred: boolean
|
||||||
|
feed: Feed
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Entries {
|
||||||
|
total: number
|
||||||
|
entries: Entry[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const APIError = () => new Error(intl.get("service.failure"));
|
||||||
|
|
||||||
|
// base endpoint, authorization with dedicated token or http basic user/pass pair
|
||||||
|
async function fetchAPI(configs: MinifluxConfigs, endpoint: string = "", method: string = "GET", body: string = null): Promise<Response>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const headers = new Headers();
|
||||||
|
headers.append("content-type", "application/x-www-form-urlencoded");
|
||||||
|
|
||||||
|
configs.apiKeyAuth ?
|
||||||
|
headers.append("X-Auth-Token", configs.authKey)
|
||||||
|
:
|
||||||
|
headers.append("Authorization", `Basic ${configs.authKey}`)
|
||||||
|
|
||||||
|
const response = await fetch(configs.endpoint + "/v1/" + endpoint, {
|
||||||
|
method: method,
|
||||||
|
body: body,
|
||||||
|
headers: headers
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
console.log(error);
|
||||||
|
throw APIError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const minifluxServiceHooks: ServiceHooks = {
|
||||||
|
|
||||||
|
// poll service info endpoint to verify auth
|
||||||
|
authenticate: async (configs: MinifluxConfigs) => {
|
||||||
|
const response = await fetchAPI(configs, "me");
|
||||||
|
|
||||||
|
if (await response.json().then(json => json.error_message))
|
||||||
|
return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
|
||||||
|
// collect sources from service, along with associated groups/categories
|
||||||
|
updateSources: () => async (dispatch, getState) => {
|
||||||
|
const configs = getState().service as MinifluxConfigs
|
||||||
|
|
||||||
|
// fetch and create groups in redux
|
||||||
|
if (configs.importGroups)
|
||||||
|
{
|
||||||
|
const groups: Category[] = await fetchAPI(configs, "categories")
|
||||||
|
.then(response => response.json())
|
||||||
|
groups.forEach(group => dispatch(createSourceGroup(group.title)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch all feeds
|
||||||
|
const feedResponse = await fetchAPI(configs, "feeds")
|
||||||
|
const feeds = await feedResponse.json()
|
||||||
|
|
||||||
|
if (feeds === undefined) throw APIError()
|
||||||
|
|
||||||
|
// go through feeds, create typed source while also mapping by group
|
||||||
|
let sources: RSSSource[] = new Array<RSSSource>();
|
||||||
|
let groupsMap: Map<string, string> = new Map<string, string>()
|
||||||
|
for (let feed of feeds)
|
||||||
|
{
|
||||||
|
let source = new RSSSource(feed.feed_url, feed.title);
|
||||||
|
// associate service christened id to match in other request
|
||||||
|
source.serviceRef = feed.id.toString();
|
||||||
|
sources.push(source);
|
||||||
|
groupsMap.set(feed.id.toString(), feed.category.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
return [sources, groupsMap]
|
||||||
|
},
|
||||||
|
|
||||||
|
// fetch entries from after the last fetched id (if exists)
|
||||||
|
// limit by quantity and maximum safe integer (id)
|
||||||
|
// NOTE: miniflux endpoint /entries default order with "published at", and does not offer "created_at"
|
||||||
|
// but does offer id sort, directly correlated with "created". some feeds give strange published_at.
|
||||||
|
|
||||||
|
fetchItems: () => async (_, getState) => {
|
||||||
|
const state = getState()
|
||||||
|
const configs = state.service as MinifluxConfigs
|
||||||
|
let items: Entry[] = new Array()
|
||||||
|
let entriesResponse: Entries
|
||||||
|
|
||||||
|
// parameters
|
||||||
|
let min = Number.MAX_SAFE_INTEGER
|
||||||
|
configs.lastId ? configs.lastId : 0
|
||||||
|
// intermediate
|
||||||
|
const quantity = 100;
|
||||||
|
let continueId: number
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (continueId)
|
||||||
|
{
|
||||||
|
entriesResponse = await fetchAPI(configs, `entries?
|
||||||
|
order=id
|
||||||
|
&direction=desc
|
||||||
|
&after_entry_id=${configs.lastId}
|
||||||
|
&before_entry_id=${continueId}
|
||||||
|
&limit=${quantity}`).then(response => response.json());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
entriesResponse = await fetchAPI(configs, `entries?
|
||||||
|
order=id
|
||||||
|
&direction=desc
|
||||||
|
&after_entry_id=${configs.lastId}
|
||||||
|
&limit=${quantity}`).then(response => response.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
items = entriesResponse.entries.concat(items)
|
||||||
|
continueId = items[items.length-1].id
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (min > configs.lastId &&
|
||||||
|
entriesResponse.entries &&
|
||||||
|
entriesResponse.total == 100 &&
|
||||||
|
items.length < configs.fetchLimit)
|
||||||
|
|
||||||
|
// break/return nothing if no new items acquired
|
||||||
|
if (items.length == 0) return [[], configs]
|
||||||
|
configs.lastId = items[0].id;
|
||||||
|
|
||||||
|
// get sources that possess ref/id given by service, associate new items
|
||||||
|
const sourceMap = new Map<string, RSSSource>()
|
||||||
|
for (let source of Object.values(state.sources)) {
|
||||||
|
if (source.serviceRef) {
|
||||||
|
sourceMap.set(source.serviceRef, source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// map item objects to rssitem type while appling rules (if exist)
|
||||||
|
const parsedItems = items.map(item => {
|
||||||
|
const source = sourceMap.get(item.feed.id.toString())
|
||||||
|
|
||||||
|
let parsedItem = {
|
||||||
|
source: source.sid,
|
||||||
|
title: item.title,
|
||||||
|
link: item.url,
|
||||||
|
date: new Date(item.created_at),
|
||||||
|
fetchedDate: new Date(),
|
||||||
|
content: item.content,
|
||||||
|
snippet: htmlDecode(item.content).trim(),
|
||||||
|
creator: item.author,
|
||||||
|
hasRead: Boolean(item.status == "read"),
|
||||||
|
starred: Boolean(item.starred),
|
||||||
|
hidden: false,
|
||||||
|
notify: false,
|
||||||
|
serviceRef: String(item.id),
|
||||||
|
} as RSSItem
|
||||||
|
|
||||||
|
|
||||||
|
// Try to get the thumbnail of the item
|
||||||
|
let dom = domParser.parseFromString(item.content, "text/html")
|
||||||
|
let baseEl = dom.createElement("base")
|
||||||
|
baseEl.setAttribute(
|
||||||
|
"href",
|
||||||
|
parsedItem.link.split("/").slice(0, 3).join("/")
|
||||||
|
)
|
||||||
|
dom.head.append(baseEl)
|
||||||
|
let img = dom.querySelector("img")
|
||||||
|
if (img && img.src)
|
||||||
|
parsedItem.thumb = img.src
|
||||||
|
|
||||||
|
|
||||||
|
if (source.rules)
|
||||||
|
{
|
||||||
|
SourceRule.applyAll(source.rules, parsedItem)
|
||||||
|
if (Boolean(item.status == "read") !== parsedItem.hasRead)
|
||||||
|
minifluxServiceHooks.markRead(parsedItem)
|
||||||
|
if (Boolean(item.starred) !== Boolean(parsedItem.starred))
|
||||||
|
minifluxServiceHooks.markUnread(parsedItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedItem
|
||||||
|
});
|
||||||
|
|
||||||
|
return [parsedItems, configs]
|
||||||
|
},
|
||||||
|
|
||||||
|
// get remote read and star state of articles, for local sync
|
||||||
|
syncItems: () => async(_, getState) => {
|
||||||
|
const configs = getState().service as MinifluxConfigs
|
||||||
|
|
||||||
|
const unread: Entries = await fetchAPI(configs, "entries?status=unread")
|
||||||
|
.then(response => response.json());
|
||||||
|
const starred: Entries = await fetchAPI(configs, "entries?starred=true")
|
||||||
|
.then(response => response.json());
|
||||||
|
|
||||||
|
return [new Set(unread.entries.map((entry: Entry) => String(entry.id))), new Set(starred.entries.map((entry: Entry) => String(entry.id)))];
|
||||||
|
},
|
||||||
|
|
||||||
|
markRead: (item: RSSItem) => async(_, getState) => {
|
||||||
|
if (!item.serviceRef) return;
|
||||||
|
|
||||||
|
const body = `{
|
||||||
|
"entry_ids": [${item.serviceRef}],
|
||||||
|
"status": "read"
|
||||||
|
}`
|
||||||
|
|
||||||
|
const response = await fetchAPI(getState().service as MinifluxConfigs, "entries", "PUT", body)
|
||||||
|
|
||||||
|
if (response.status !== 204) throw APIError();
|
||||||
|
},
|
||||||
|
|
||||||
|
markUnread: (item: RSSItem) => async (_, getState) => {
|
||||||
|
if (!item.serviceRef) return;
|
||||||
|
|
||||||
|
const body = `{
|
||||||
|
"entry_ids": [${item.serviceRef}],
|
||||||
|
"status": "unread"
|
||||||
|
}`
|
||||||
|
await fetchAPI(getState().service as MinifluxConfigs, "entries", "PUT", body)
|
||||||
|
},
|
||||||
|
|
||||||
|
// mark entries for source ids as read, relative to date, determined by "before" bool
|
||||||
|
|
||||||
|
// context menu component:
|
||||||
|
// item - null, item date, either
|
||||||
|
// group - group sources, null, true
|
||||||
|
// nav - null, daysago, true
|
||||||
|
|
||||||
|
// if null, state consulted for context sids
|
||||||
|
|
||||||
|
markAllRead: (sids, date, before) => async(_, getState) => {
|
||||||
|
|
||||||
|
const state = getState()
|
||||||
|
let items = state.feeds[state.page.feedId].iids
|
||||||
|
.map(iid => state.items[iid])
|
||||||
|
.filter(item => item.serviceRef && !item.hasRead)
|
||||||
|
|
||||||
|
if (date) items = items.filter(i => before ? i.date < date : i.date > date)
|
||||||
|
|
||||||
|
const refs = items.map(item => item.serviceRef)
|
||||||
|
|
||||||
|
const body = `{
|
||||||
|
"entry_ids": [${refs}],
|
||||||
|
"status": "read"
|
||||||
|
}`
|
||||||
|
|
||||||
|
await fetchAPI(getState().service as MinifluxConfigs, "entries", "PUT", body)
|
||||||
|
},
|
||||||
|
|
||||||
|
star: (item: RSSItem) => async (_, getState) => {
|
||||||
|
if (!item.serviceRef) return;
|
||||||
|
|
||||||
|
await fetchAPI(getState().service as MinifluxConfigs, `entries/${item.serviceRef}/bookmark`, "PUT");
|
||||||
|
},
|
||||||
|
|
||||||
|
unstar: (item: RSSItem) => async (_, getState) => {
|
||||||
|
if (!item.serviceRef) return;
|
||||||
|
|
||||||
|
await fetchAPI(getState().service as MinifluxConfigs, `entries/${item.serviceRef}/bookmark`, "PUT");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user