keep items until switching feeds

This commit is contained in:
刘浩远 2020-09-01 18:31:14 +08:00
parent 110be62694
commit 1d8863922f
22 changed files with 112 additions and 60 deletions

View File

@ -119,7 +119,7 @@
}
.default-card img.bg {
object-fit: cover;
filter: saturate(150%) blur(20px);
filter: var(--blur);
}
.default-card div.bg {
background-color: #fffb;
@ -230,6 +230,9 @@
height: 100%;
border-left: 2px solid var(--primary);
}
.list-card.read, .list-card.read p.snippet {
color: var(--neutralSecondaryAlt);
}
.magazine-card {
width: 700px;

View File

@ -173,9 +173,11 @@
flex-wrap: wrap;
justify-content: space-around;
padding: 12px;
height: calc(100% - 56px);
height: calc(100% - 32px);
overflow: hidden scroll;
margin-top: var(--navHeight);
width: 100%;
box-sizing: border-box;
}
.cards-feed-container .ms-List-page {
display: flex;

View File

@ -1,5 +1,5 @@
:root {
--neutralLighterAltOpacity: rgba(250, 249, 248, 0.8);
--neutralLighterAltOpacity: #faf9f8cc;
--neutralLighterAlt: #faf9f8;
--neutralLighter: #f3f2f1;
--neutralLight: #edebe9;
@ -18,13 +18,12 @@
--whiteConstant: #fff;
--primary: #0078d4;
--navHeight: 32px;
--blur-0: saturate(150%) blur(16px);
--blur-1: saturate(150%) blur(32px);
--transition-timing: cubic-bezier(0.1, 0.9, 0.2, 1);
--blur: saturate(150%) blur(20px);
}
@media (prefers-color-scheme: dark) {
:root {
--neutralLighterAltOpacity: rgba(40, 40, 40, 0.8);
--neutralLighterAltOpacity: #282828cc;
--neutralLighterAlt: #282828;
--neutralLighter: #313131;
--neutralLight: #3f3f3f;
@ -54,6 +53,9 @@ html, body {
overflow: hidden;
margin: 0;
}
body.win32, body.linux {
background-color: var(--neutralLighterAlt);
}
#root {
height: 100%;
}

16
dist/styles/main.css vendored
View File

@ -18,13 +18,13 @@
height: 100%;
}
.article-container {
backdrop-filter: saturate(150%) blur(20px);
backdrop-filter: var(--blur);
animation-name: fade;
background-color: #0008;
}
.menu-container, .article-container, .article-wrapper {
animation-duration: 0.5s;
animation-timing-function: cubic-bezier(0.1, 0.9, 0.2, 1);
animation-timing-function: var(--transition-timing);
animation-fill-mode: both;
}
.menu-container {
@ -43,13 +43,15 @@
width: 280px;
height: 100%;
background-color: var(--neutralLighterAltOpacity);
backdrop-filter: var(--blur-1);
backdrop-filter: var(--blur);
box-shadow: 5px 0 25px #0004;
transition: clip-path cubic-bezier(0.1, 0.9, 0.2, 1) .367s;
clip-path: inset(0 100% 0 0);;
transition: clip-path var(--transition-timing) .367s, opacity cubic-bezier(0, 0, 0.2, 1) .367s;
clip-path: inset(0 100% 0 0);
opacity: 0;
}
.menu-container.show .menu {
clip-path: inset(0 -50px 0 0);;
clip-path: inset(0 -50px 0 0);
opacity: 1;
}
body.blur .menu .btn-group {
--black: var(--neutralSecondaryAlt);
@ -235,7 +237,7 @@ body.darwin .list-main .article-search {
margin: 0 10px;
}
.main, .list-main {
transition: margin-left cubic-bezier(0.1, 0.9, 0.2, 1) .367s;
transition: margin-left var(--transition-timing) .367s;
margin-left: 0;
}

View File

@ -132,6 +132,9 @@ class Article extends React.Component<ArticleProps, ArticleState> {
case "w": case "W":
this.toggleFull()
break
case "H": case "h":
this.props.toggleHidden(this.props.item)
break
default:
const keyboardEvent = new KeyboardEvent("keydown", {
code: input.code,

View File

@ -8,6 +8,7 @@ const className = (props: Card.Props) => {
let cn = ["card", "list-card"]
if (props.item.hidden) cn.push("hidden")
if (props.selected) cn.push("selected")
if ((props.viewConfigs & ViewConfigs.FadeRead) && props.item.hasRead) cn.push("read")
return cn.join(" ")
}

View File

@ -170,6 +170,13 @@ export class ContextMenu extends React.Component<ContextMenuProps> {
checked: Boolean(this.props.viewConfigs & ViewConfigs.ShowSnippet),
onClick: () => this.props.setViewConfigs(this.props.viewConfigs ^ ViewConfigs.ShowSnippet)
},
{
key: "fadeRead",
text: intl.get("context.fadeRead"),
canCheck: true,
checked: Boolean(this.props.viewConfigs & ViewConfigs.FadeRead),
onClick: () => this.props.setViewConfigs(this.props.viewConfigs ^ ViewConfigs.FadeRead)
}
]
}
},

View File

@ -16,7 +16,7 @@ export type MenuProps = {
searchOn: boolean,
itemOn: boolean,
toggleMenu: () => void,
allArticles: () => void,
allArticles: (init?: boolean) => void,
selectSourceGroup: (group: SourceGroup, menuKey: string) => void,
selectSource: (source: RSSSource) => void,
groupContextMenu: (sids: number[], event: React.MouseEvent) => void,
@ -43,7 +43,7 @@ export class Menu extends React.Component<MenuProps> {
ariaLabel: this.countOverflow(Object.values(this.props.sources).map(s => s.unreadCount).reduce((a, b) => a + b, 0)),
key: ALL,
icon: "TextDocument",
onClick: this.props.allArticles,
onClick: () => this.props.allArticles(this.props.selected !== ALL),
url: null
}
]

View File

@ -36,6 +36,8 @@ class FeedbinConfigsTab extends React.Component<ServiceConfigsTabProps, FeedbinC
{ 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 })

View File

@ -37,6 +37,8 @@ class FeverConfigsTab extends React.Component<ServiceConfigsTabProps, FeverConfi
{ 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 })

View File

@ -33,7 +33,10 @@ const mapDispatchToProps = (dispatch: AppDispatch) => {
offsetItem: (offset: number) => dispatch(showOffsetItem(offset)),
toggleHasRead: (item: RSSItem) => dispatch(item.hasRead ? markUnread(item) : markRead(item)),
toggleStarred: (item: RSSItem) => dispatch(toggleStarred(item)),
toggleHidden: (item: RSSItem) => dispatch(toggleHidden(item)),
toggleHidden: (item: RSSItem) => {
if (!item.hidden) dispatch(dismissItem())
dispatch(toggleHidden(item))
},
textMenu: (position: [number, number], text: string, url: string) => dispatch(openTextMenu(position, text, url)),
imageMenu: (position: [number, number]) => dispatch(openImageMenu(position)),
dismissContextMenu: () => dispatch(closeContextMenu())

View File

@ -31,8 +31,8 @@ const mapStateToProps = createSelector(
const mapDispatchToProps = dispatch => ({
toggleMenu: () => dispatch(toggleMenu()),
allArticles: () => {
dispatch(selectAllArticles()),
allArticles: (init = false) => {
dispatch(selectAllArticles(init)),
dispatch(initFeeds())
},
selectSourceGroup: (group: SourceGroup, menuKey: string) => {

View File

@ -9,11 +9,7 @@ export default function performUpdate(store: Store<SchemaTypes>) {
if (useNeDB === undefined && version !== null) {
const revs = version.split(".").map(s => parseInt(s))
if ((revs[0] === 0 && revs[1] < 8) || !app.isPackaged) {
store.set("useNeDB", true)
} else {
store.set("useNeDB", false)
}
store.set("useNeDB", (revs[0] === 0 && revs[1] < 8) || !app.isPackaged)
}
if (version != currentVersion) {
store.set("version", currentVersion)

View File

@ -25,6 +25,7 @@ export const enum ViewType {
export const enum ViewConfigs {
ShowCover = 1 << 0,
ShowSnippet = 1 << 1,
FadeRead = 1 << 2,
}
export const enum ThemeSettings {

View File

@ -91,7 +91,8 @@
"copyImageURL": "Copy image link",
"caseSensitive": "Case sensitive",
"showCover": "Show cover",
"showSnippet": "Show snippet"
"showSnippet": "Show snippet",
"fadeRead": "Fade read articles"
},
"searchEngine": {
"name": "Search engine",
@ -198,7 +199,8 @@
"fetchLimitNum": "{count} latest articles",
"importGroups": "Import groups",
"failure": "Cannot connect to service",
"failureHint": "Please check the service configuration or network status."
"failureHint": "Please check the service configuration or network status.",
"fetchUnlimited": "Unlimited (not recommended)"
},
"app": {
"cleanup": "Clean up",

View File

@ -91,7 +91,8 @@
"copyImageURL": "复制图像链接",
"caseSensitive": "区分大小写",
"showCover": "显示封面",
"showSnippet": "显示摘要"
"showSnippet": "显示摘要",
"fadeRead": "淡化已读文章"
},
"searchEngine": {
"name": "搜索引擎",
@ -196,7 +197,8 @@
"fetchLimitNum": "最近 {count} 篇文章",
"importGroups": "导入分组",
"failure": "连接到服务时出错",
"failureHint": "请检查服务配置或网络连接"
"failureHint": "请检查服务配置或网络连接",
"fetchUnlimited": "无限制(不建议)"
},
"app": {
"cleanup": "清理",

View File

@ -136,6 +136,7 @@ export interface MenuActionTypes {
export const TOGGLE_SETTINGS = "TOGGLE_SETTINGS"
export const SAVE_SETTINGS = "SAVE_SETTINGS"
export const FREE_MEMORY = "FREE_MEMORY"
interface ToggleSettingsAction {
type: typeof TOGGLE_SETTINGS
@ -145,7 +146,11 @@ interface ToggleSettingsAction {
interface SaveSettingsAction {
type: typeof SAVE_SETTINGS
}
export type SettingsActionTypes = ToggleSettingsAction | SaveSettingsAction
interface FreeMemoryAction {
type: typeof FREE_MEMORY
iids: Set<number>
}
export type SettingsActionTypes = ToggleSettingsAction | SaveSettingsAction | FreeMemoryAction
export function closeContextMenu(): AppThunk {
return (dispatch, getState) => {
@ -205,15 +210,15 @@ export const toggleSettings = (open = true, sids = new Array<number>()) => ({
sids: sids,
})
export function exitSettings(): AppThunk {
return (dispatch, getState) => {
export function exitSettings(): AppThunk<Promise<void>> {
return async (dispatch, getState) => {
if (!getState().app.settings.saving) {
if (getState().app.settings.changed) {
dispatch(saveSettings())
dispatch(selectAllArticles(true))
dispatch(initFeeds(true)).then(() =>
dispatch(toggleSettings(false))
)
await dispatch(initFeeds(true))
dispatch(toggleSettings(false))
freeMemory()
} else {
dispatch(toggleSettings(false))
}
@ -221,6 +226,19 @@ export function exitSettings(): AppThunk {
}
}
function freeMemory(): AppThunk {
return (dispatch, getState) => {
const iids = new Set<number>()
for (let feed of Object.values(getState().feeds)) {
if (feed.loaded) feed.iids.forEach(iids.add, iids)
}
dispatch({
type: FREE_MEMORY,
iids: iids
})
}
}
let fetchTimeout: NodeJS.Timeout
export function setupAutoFetch(): AppThunk {
return (dispatch, getState) => {

View File

@ -96,13 +96,13 @@ export class RSSFeed {
this.filter = filter === null ? new FeedFilter() : filter
}
static async loadFeed(feed: RSSFeed, init = false): Promise<RSSItem[]> {
static async loadFeed(feed: RSSFeed, skip = 0): Promise<RSSItem[]> {
const predicates = FeedFilter.toPredicates(feed.filter)
predicates.push(db.items.source.in(feed.sids))
return (await db.itemsDB.select().from(db.items).where(
lf.op.and.apply(null, predicates)
).orderBy(db.items.date, lf.Order.DESC)
.skip(init ? 0 : feed.iids.length)
.skip(skip)
.limit(LOAD_QUANTITY)
.exec()) as RSSItem[]
}
@ -175,7 +175,7 @@ export function initFeeds(force = false): AppThunk<Promise<void>> {
let promises = new Array<Promise<void>>()
for (let feed of Object.values(getState().feeds)) {
if (!feed.loaded || force) {
let p = RSSFeed.loadFeed(feed, force).then(items => {
let p = RSSFeed.loadFeed(feed).then(items => {
dispatch(initFeedSuccess(feed, items))
}).catch(err => {
console.log(err)
@ -217,10 +217,12 @@ export function loadMoreFailure(feed: RSSFeed, err): FeedActionTypes {
}
export function loadMore(feed: RSSFeed): AppThunk<Promise<void>> {
return (dispatch) => {
return (dispatch, getState) => {
if (feed.loaded && !feed.loading && !feed.allLoaded) {
dispatch(loadMoreRequest(feed))
return RSSFeed.loadFeed(feed).then(items => {
const state = getState()
const skipNum = feed.iids.filter(i => FeedFilter.testItem(feed.filter, state.items[i])).length
return RSSFeed.loadFeed(feed, skipNum).then(items => {
dispatch(loadMoreSuccess(feed, items))
}).catch(e => {
console.log(e)
@ -331,9 +333,6 @@ export function feedReducer(
}
default: return state
}
case MARK_READ:
case MARK_UNREAD:
case TOGGLE_STARRED:
case TOGGLE_HIDDEN: {
let nextItem = applyItemReduction(action.item, action.type)
let filteredFeeds = Object.values(state).filter(feed => feed.loaded && !FeedFilter.testItem(feed.filter, nextItem))

View File

@ -5,7 +5,7 @@ import { domParser, htmlDecode, ActionStatus, AppThunk, platformCtrl } from "../
import { RSSSource, updateSource, updateUnreadCounts } from "./source"
import { FeedActionTypes, INIT_FEED, LOAD_MORE, FilterType, initFeeds } from "./feed"
import Parser from "@yang991178/rss-parser"
import { pushNotification, setupAutoFetch } from "./app"
import { pushNotification, setupAutoFetch, SettingsActionTypes, FREE_MEMORY } from "./app"
import { getServiceHooks, syncWithService, ServiceActionTypes, SYNC_LOCAL_ITEMS } from "./service"
export class RSSItem {
@ -281,9 +281,6 @@ export function markAllRead(sids: number[] = null, date: Date = null, before = t
sids: sids
})
}
if (!(state.page.filter.type & FilterType.ShowRead)) {
dispatch(initFeeds(true))
}
}
}
@ -380,7 +377,7 @@ export function applyItemReduction(item: RSSItem, type: string) {
export function itemReducer(
state: ItemState = {},
action: ItemActionTypes | FeedActionTypes | ServiceActionTypes
action: ItemActionTypes | FeedActionTypes | ServiceActionTypes | SettingsActionTypes
): ItemState {
switch (action.type) {
case FETCH_ITEMS:
@ -446,6 +443,13 @@ export function itemReducer(
}
return nextState
}
case FREE_MEMORY: {
const nextState: ItemState = {}
for (let item of Object.values(state)) {
if (action.iids.has(item._id)) nextState[item._id] = item
}
return nextState
}
default: return state
}
}

View File

@ -166,9 +166,6 @@ function syncItems(hook: ServiceHooks["syncItems"]): AppThunk<Promise<void>> {
await db.itemsDB.createTransaction().exec(updates)
await dispatch(updateUnreadCounts())
dispatch(syncLocalItems(unreadCopy, starredCopy))
if (!(state.page.filter.type & FilterType.ShowRead) || !(state.page.filter.type & FilterType.ShowNotStarred)) {
dispatch(initFeeds(true))
}
}
}
}

View File

@ -107,12 +107,16 @@ export const feedbinServiceHooks: ServiceHooks = {
let min = Number.MAX_SAFE_INTEGER
let lastFetched: any[]
do {
const response = await fetchAPI(configs, "entries.json?mode=extended&per_page=125&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
try {
const response = await fetchAPI(configs, "entries.json?mode=extended&per_page=125&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
} catch {
break
}
} while (
min > configs.lastId &&
lastFetched && lastFetched.length >= 125 &&
@ -133,7 +137,9 @@ export const feedbinServiceHooks: ServiceHooks = {
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 parsedItems = new Array<RSSItem>()
items.forEach(i => {
if (i.content === null) return
const source = fidMap.get(String(i.feed_id))
const dom = domParser.parseFromString(i.content, "text/html")
const item = {
@ -166,7 +172,7 @@ export const feedbinServiceHooks: ServiceHooks = {
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
parsedItems.push(item)
})
return [parsedItems, configs]
} else {

View File

@ -181,8 +181,8 @@ export function initSources(): AppThunk<Promise<void>> {
source.unreadCount = 0
state[source.sid] = source
}
await unreadCount(sources)
dispatch(initSourcesSuccess(sources))
await unreadCount(state)
dispatch(initSourcesSuccess(state))
}
}