update dependencies and miniflux support
This commit is contained in:
parent
49b9a213e3
commit
7c31c65fe9
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "fluent-reader",
|
||||
"version": "1.1.2",
|
||||
"version": "1.1.3",
|
||||
"description": "Modern desktop RSS reader",
|
||||
"main": "./dist/electron.js",
|
||||
"scripts": {
|
||||
|
@ -26,7 +26,7 @@
|
|||
"@types/react-dom": "^16.9.8",
|
||||
"@types/react-redux": "^7.1.9",
|
||||
"@yang991178/rss-parser": "^3.8.1",
|
||||
"electron": "^19.0.0",
|
||||
"electron": "^21.0.1",
|
||||
"electron-builder": "^23.0.3",
|
||||
"electron-react-devtools": "^0.5.3",
|
||||
"electron-store": "^5.2.0",
|
||||
|
|
|
@ -42,7 +42,7 @@ export class ServiceTab extends React.Component<
|
|||
{ key: SyncService.Feedbin, text: "Feedbin" },
|
||||
{ key: SyncService.GReader, text: "Google Reader API (Beta)" },
|
||||
{ key: SyncService.Inoreader, text: "Inoreader" },
|
||||
{ key: SyncService.Miniflux, text: "Miniflux" },
|
||||
{ key: SyncService.Miniflux, text: "Miniflux" },
|
||||
{ key: -1, text: intl.get("service.suggest") },
|
||||
]
|
||||
|
||||
|
@ -90,13 +90,13 @@ export class ServiceTab extends React.Component<
|
|||
exit={this.exitConfigsTab}
|
||||
/>
|
||||
)
|
||||
case SyncService.Miniflux:
|
||||
return (
|
||||
<MinifluxConfigsTab
|
||||
{...this.props}
|
||||
exit={this.exitConfigsTab}
|
||||
/>
|
||||
)
|
||||
case SyncService.Miniflux:
|
||||
return (
|
||||
<MinifluxConfigsTab
|
||||
{...this.props}
|
||||
exit={this.exitConfigsTab}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
|
|
|
@ -23,10 +23,10 @@ import { MinifluxConfigs } from "../../../scripts/models/services/miniflux"
|
|||
type MinifluxConfigsTabState = {
|
||||
existing: boolean
|
||||
endpoint: string
|
||||
apiKeyAuth: boolean
|
||||
username: string
|
||||
password: string
|
||||
apiKey: string
|
||||
apiKeyAuth: boolean
|
||||
username: string
|
||||
password: string
|
||||
apiKey: string
|
||||
fetchLimit: number
|
||||
importGroups: boolean
|
||||
}
|
||||
|
@ -37,13 +37,13 @@ class MinifluxConfigsTab extends React.Component<
|
|||
> {
|
||||
constructor(props: ServiceConfigsTabProps) {
|
||||
super(props)
|
||||
const configs = props.configs as MinifluxConfigs
|
||||
const configs = props.configs as MinifluxConfigs
|
||||
this.state = {
|
||||
existing: configs.type === SyncService.Miniflux,
|
||||
endpoint: configs.endpoint || "",
|
||||
apiKeyAuth: true,
|
||||
username: "",
|
||||
password: "",
|
||||
username: "",
|
||||
password: "",
|
||||
apiKey: "",
|
||||
fetchLimit: configs.fetchLimit || 250,
|
||||
importGroups: true,
|
||||
|
@ -65,13 +65,19 @@ class MinifluxConfigsTab extends React.Component<
|
|||
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" })
|
||||
}
|
||||
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
|
||||
|
@ -89,33 +95,39 @@ class MinifluxConfigsTab extends React.Component<
|
|||
return (
|
||||
urlTest(this.state.endpoint.trim()) &&
|
||||
(this.state.existing ||
|
||||
this.state.apiKey ||
|
||||
this.state.apiKey ||
|
||||
(this.state.username && this.state.password))
|
||||
)
|
||||
}
|
||||
|
||||
save = async () => {
|
||||
let configs: MinifluxConfigs
|
||||
let configs: MinifluxConfigs
|
||||
|
||||
if (this.state.existing)
|
||||
{
|
||||
if (this.state.existing) {
|
||||
configs = {
|
||||
...this.props.configs,
|
||||
endpoint: this.state.endpoint,
|
||||
fetchLimit: this.state.fetchLimit,
|
||||
} as MinifluxConfigs
|
||||
} 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
|
||||
{
|
||||
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'),
|
||||
authKey: this.state.apiKeyAuth
|
||||
? this.state.apiKey
|
||||
: Buffer.from(
|
||||
this.state.username + ":" + this.state.password,
|
||||
"binary"
|
||||
).toString("base64"),
|
||||
fetchLimit: this.state.fetchLimit,
|
||||
}
|
||||
|
||||
|
@ -160,9 +172,7 @@ class MinifluxConfigsTab extends React.Component<
|
|||
userSelect: "none",
|
||||
}}
|
||||
/>
|
||||
<Label style={{ margin: "8px 0 36px" }}>
|
||||
Miniflux
|
||||
</Label>
|
||||
<Label style={{ margin: "8px 0 36px" }}>Miniflux</Label>
|
||||
<Stack className="login-form" horizontal>
|
||||
<Stack.Item>
|
||||
<Label>{intl.get("service.endpoint")}</Label>
|
||||
|
@ -188,66 +198,76 @@ class MinifluxConfigsTab extends React.Component<
|
|||
<Stack.Item grow>
|
||||
<Dropdown
|
||||
options={this.authenticationOptions()}
|
||||
selectedKey={this.state.apiKeyAuth ? "apiKey" : "userPass" }
|
||||
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> }
|
||||
{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>
|
||||
|
@ -295,13 +315,10 @@ class MinifluxConfigsTab extends React.Component<
|
|||
)}
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
{this.state.existing && (
|
||||
<LiteExporter serviceConfigs={this.props.configs} />
|
||||
)}
|
||||
</Stack>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default MinifluxConfigsTab
|
||||
export default MinifluxConfigsTab
|
||||
|
|
|
@ -59,7 +59,7 @@ export const enum SyncService {
|
|||
Feedbin,
|
||||
GReader,
|
||||
Inoreader,
|
||||
Miniflux,
|
||||
Miniflux,
|
||||
}
|
||||
export interface ServiceConfigs {
|
||||
type: SyncService
|
||||
|
|
|
@ -18,7 +18,7 @@ import { createSourceGroup, addSourceToGroup } from "./group"
|
|||
import { feverServiceHooks } from "./services/fever"
|
||||
import { feedbinServiceHooks } from "./services/feedbin"
|
||||
import { gReaderServiceHooks } from "./services/greader"
|
||||
import { minifluxServiceHooks} from "./services/miniflux"
|
||||
import { minifluxServiceHooks } from "./services/miniflux"
|
||||
|
||||
export interface ServiceHooks {
|
||||
authenticate?: (configs: ServiceConfigs) => Promise<boolean>
|
||||
|
@ -46,8 +46,8 @@ export function getServiceHooksFromType(type: SyncService): ServiceHooks {
|
|||
case SyncService.GReader:
|
||||
case SyncService.Inoreader:
|
||||
return gReaderServiceHooks
|
||||
case SyncService.Miniflux:
|
||||
return minifluxServiceHooks
|
||||
case SyncService.Miniflux:
|
||||
return minifluxServiceHooks
|
||||
default:
|
||||
return {}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import intl from "react-intl-universal"
|
||||
import * as db from "../../db"
|
||||
import lf from "lovefield"
|
||||
import { ServiceHooks } from "../service"
|
||||
import { ServiceConfigs, SyncService } from "../../../schema-types"
|
||||
import { createSourceGroup } from "../group"
|
||||
|
@ -11,311 +13,343 @@ import { SourceRule } from "../rule"
|
|||
export interface MinifluxConfigs extends ServiceConfigs {
|
||||
type: SyncService.Miniflux
|
||||
endpoint: string
|
||||
apiKeyAuth: boolean
|
||||
apiKeyAuth: boolean
|
||||
authKey: string
|
||||
fetchLimit: number
|
||||
lastId?: number
|
||||
lastId?: number
|
||||
}
|
||||
|
||||
// partial api schema
|
||||
interface Feed {
|
||||
id: number
|
||||
feed_url: string
|
||||
title: string
|
||||
category: { title: string }
|
||||
id: number
|
||||
feed_url: string
|
||||
title: string
|
||||
category: { title: string }
|
||||
}
|
||||
|
||||
interface Category {
|
||||
title: string
|
||||
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
|
||||
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[]
|
||||
total: number
|
||||
entries: Entry[]
|
||||
}
|
||||
|
||||
const APIError = () => new Error(intl.get("service.failure"));
|
||||
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");
|
||||
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}`)
|
||||
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();
|
||||
}
|
||||
let baseUrl = configs.endpoint
|
||||
if (!baseUrl.endsWith("/")) baseUrl = baseUrl + "/"
|
||||
if (!baseUrl.endsWith("/v1/")) baseUrl = baseUrl + "v1/"
|
||||
const response = await fetch(baseUrl + 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")
|
||||
|
||||
// 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
|
||||
if (await response.json().then(json => json.error_message)) return false
|
||||
|
||||
return true
|
||||
},
|
||||
return true
|
||||
},
|
||||
|
||||
// collect sources from service, along with associated groups/categories
|
||||
updateSources: () => async (dispatch, getState) => {
|
||||
const configs = getState().service as MinifluxConfigs
|
||||
// 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 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()
|
||||
// fetch all feeds
|
||||
const feedResponse = await fetchAPI(configs, "feeds")
|
||||
const feeds = await feedResponse.json()
|
||||
|
||||
if (feeds === undefined) throw APIError()
|
||||
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)
|
||||
}
|
||||
// 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]
|
||||
},
|
||||
return [sources, configs.importGroups ? groupsMap : undefined]
|
||||
},
|
||||
|
||||
// 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
|
||||
// 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.
|
||||
|
||||
// parameters
|
||||
let min = Number.MAX_SAFE_INTEGER
|
||||
configs.lastId ? configs.lastId : 0
|
||||
// intermediate
|
||||
const quantity = 100;
|
||||
let continueId: number
|
||||
fetchItems: () => async (_, getState) => {
|
||||
const state = getState()
|
||||
const configs = state.service as MinifluxConfigs
|
||||
let items: Entry[] = new Array()
|
||||
let entriesResponse: Entries
|
||||
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
if (continueId)
|
||||
{
|
||||
entriesResponse = await fetchAPI(configs, `entries?
|
||||
// parameters
|
||||
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?
|
||||
&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());
|
||||
}
|
||||
&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)
|
||||
items = entriesResponse.entries.concat(items)
|
||||
continueId = items[items.length - 1].id
|
||||
} catch {
|
||||
break
|
||||
}
|
||||
} while (
|
||||
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;
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
// 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())
|
||||
// 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
|
||||
let parsedItem = {
|
||||
source: source.sid,
|
||||
title: item.title,
|
||||
link: item.url,
|
||||
date: new Date(item.published_at ?? 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
|
||||
|
||||
// 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 ((item.status === "read") !== parsedItem.hasRead)
|
||||
minifluxServiceHooks.markRead(parsedItem)
|
||||
if (item.starred !== parsedItem.starred)
|
||||
minifluxServiceHooks.markUnread(parsedItem)
|
||||
}
|
||||
|
||||
return parsedItem
|
||||
})
|
||||
|
||||
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 [parsedItems, configs]
|
||||
},
|
||||
|
||||
return parsedItem
|
||||
});
|
||||
// get remote read and star state of articles, for local sync
|
||||
syncItems: () => async (_, getState) => {
|
||||
const configs = getState().service as MinifluxConfigs
|
||||
|
||||
return [parsedItems, configs]
|
||||
},
|
||||
const unreadPromise: Promise<Entries> = fetchAPI(
|
||||
configs,
|
||||
"entries?status=unread"
|
||||
).then(response => response.json())
|
||||
const starredPromise: Promise<Entries> = fetchAPI(
|
||||
configs,
|
||||
"entries?starred=true"
|
||||
).then(response => response.json())
|
||||
const [unread, starred] = await Promise.all([unreadPromise, starredPromise])
|
||||
|
||||
// get remote read and star state of articles, for local sync
|
||||
syncItems: () => async(_, getState) => {
|
||||
const configs = getState().service as MinifluxConfigs
|
||||
return [
|
||||
new Set(unread.entries.map((entry: Entry) => String(entry.id))),
|
||||
new Set(starred.entries.map((entry: Entry) => String(entry.id))),
|
||||
]
|
||||
},
|
||||
|
||||
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());
|
||||
markRead: (item: RSSItem) => async (_, getState) => {
|
||||
if (!item.serviceRef) return
|
||||
|
||||
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 = `{
|
||||
const body = `{
|
||||
"entry_ids": [${item.serviceRef}],
|
||||
"status": "read"
|
||||
}`
|
||||
|
||||
const response = await fetchAPI(getState().service as MinifluxConfigs, "entries", "PUT", body)
|
||||
const response = await fetchAPI(
|
||||
getState().service as MinifluxConfigs,
|
||||
"entries",
|
||||
"PUT",
|
||||
body
|
||||
)
|
||||
|
||||
if (response.status !== 204) throw APIError();
|
||||
},
|
||||
if (response.status !== 204) throw APIError()
|
||||
},
|
||||
|
||||
markUnread: (item: RSSItem) => async (_, getState) => {
|
||||
if (!item.serviceRef) return;
|
||||
if (!item.serviceRef) return
|
||||
|
||||
const body = `{
|
||||
const body = `{
|
||||
"entry_ids": [${item.serviceRef}],
|
||||
"status": "unread"
|
||||
}`
|
||||
await fetchAPI(getState().service as MinifluxConfigs, "entries", "PUT", body)
|
||||
await fetchAPI(
|
||||
getState().service as MinifluxConfigs,
|
||||
"entries",
|
||||
"PUT",
|
||||
body
|
||||
)
|
||||
},
|
||||
|
||||
// mark entries for source ids as read, relative to date, determined by "before" bool
|
||||
// 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) => {
|
||||
// context menu component:
|
||||
// item - null, item date, either
|
||||
// group - group sources, null, true
|
||||
// nav - null, daysago, true
|
||||
|
||||
const state = getState()
|
||||
let items = state.feeds[state.page.feedId].iids
|
||||
.map(iid => state.items[iid])
|
||||
.filter(item => item.serviceRef && !item.hasRead)
|
||||
// if null, state consulted for context sids
|
||||
|
||||
if (date) items = items.filter(i => before ? i.date < date : i.date > date)
|
||||
markAllRead: (sids, date, before) => async (_, getState) => {
|
||||
let refs: string[]
|
||||
|
||||
const refs = items.map(item => item.serviceRef)
|
||||
if (date) {
|
||||
const predicates: lf.Predicate[] = [
|
||||
db.items.source.in(sids),
|
||||
db.items.hasRead.eq(false),
|
||||
db.items.serviceRef.isNotNull(),
|
||||
before ? db.items.date.lte(date) : db.items.date.gte(date),
|
||||
]
|
||||
const query = lf.op.and.apply(null, predicates)
|
||||
const rows = await db.itemsDB
|
||||
.select(db.items.serviceRef)
|
||||
.from(db.items)
|
||||
.where(query)
|
||||
.exec()
|
||||
refs = rows.map(row => row["serviceRef"])
|
||||
} else {
|
||||
const state = getState()
|
||||
const items = state.feeds[state.page.feedId].iids
|
||||
.map(iid => state.items[iid])
|
||||
.filter(item => item.serviceRef && !item.hasRead)
|
||||
refs = items.map(item => item.serviceRef)
|
||||
}
|
||||
|
||||
const body = `{
|
||||
const body = `{
|
||||
"entry_ids": [${refs}],
|
||||
"status": "read"
|
||||
}`
|
||||
|
||||
await fetchAPI(getState().service as MinifluxConfigs, "entries", "PUT", body)
|
||||
},
|
||||
await fetchAPI(
|
||||
getState().service as MinifluxConfigs,
|
||||
"entries",
|
||||
"PUT",
|
||||
body
|
||||
)
|
||||
},
|
||||
|
||||
star: (item: RSSItem) => async (_, getState) => {
|
||||
if (!item.serviceRef) return;
|
||||
if (!item.serviceRef) return
|
||||
|
||||
await fetchAPI(getState().service as MinifluxConfigs, `entries/${item.serviceRef}/bookmark`, "PUT");
|
||||
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");
|
||||
}
|
||||
if (!item.serviceRef) return
|
||||
|
||||
await fetchAPI(
|
||||
getState().service as MinifluxConfigs,
|
||||
`entries/${item.serviceRef}/bookmark`,
|
||||
"PUT"
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue