diff --git a/dist/styles/cards.css b/dist/styles/cards.css index 44c0dae..ca523fc 100644 --- a/dist/styles/cards.css +++ b/dist/styles/cards.css @@ -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; diff --git a/dist/styles/feeds.css b/dist/styles/feeds.css index 4bad183..d826ee3 100644 --- a/dist/styles/feeds.css +++ b/dist/styles/feeds.css @@ -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; diff --git a/dist/styles/global.css b/dist/styles/global.css index 60905eb..729f131 100644 --- a/dist/styles/global.css +++ b/dist/styles/global.css @@ -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%; } diff --git a/dist/styles/main.css b/dist/styles/main.css index 0eb0a97..3e25699 100644 --- a/dist/styles/main.css +++ b/dist/styles/main.css @@ -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; } diff --git a/src/components/article.tsx b/src/components/article.tsx index 184645d..a48a756 100644 --- a/src/components/article.tsx +++ b/src/components/article.tsx @@ -132,6 +132,9 @@ class Article extends React.Component { 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, diff --git a/src/components/cards/list-card.tsx b/src/components/cards/list-card.tsx index 914ee80..156ecbc 100644 --- a/src/components/cards/list-card.tsx +++ b/src/components/cards/list-card.tsx @@ -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(" ") } diff --git a/src/components/context-menu.tsx b/src/components/context-menu.tsx index 08e08f8..4fae212 100644 --- a/src/components/context-menu.tsx +++ b/src/components/context-menu.tsx @@ -170,6 +170,13 @@ export class ContextMenu extends React.Component { 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) + } ] } }, diff --git a/src/components/menu.tsx b/src/components/menu.tsx index a65cad9..fc8444a 100644 --- a/src/components/menu.tsx +++ b/src/components/menu.tsx @@ -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 { 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 } ] diff --git a/src/components/settings/services/feedbin.tsx b/src/components/settings/services/feedbin.tsx index d7493f1..47ed497 100644 --- a/src/components/settings/services/feedbin.tsx +++ b/src/components/settings/services/feedbin.tsx @@ -36,6 +36,8 @@ class FeedbinConfigsTab extends React.Component { this.setState({ fetchLimit: option.key as number }) diff --git a/src/components/settings/services/fever.tsx b/src/components/settings/services/fever.tsx index 3712157..a9d00f7 100644 --- a/src/components/settings/services/fever.tsx +++ b/src/components/settings/services/fever.tsx @@ -37,6 +37,8 @@ class FeverConfigsTab extends React.Component { this.setState({ fetchLimit: option.key as number }) diff --git a/src/containers/article-container.tsx b/src/containers/article-container.tsx index 15820c4..01e00d2 100644 --- a/src/containers/article-container.tsx +++ b/src/containers/article-container.tsx @@ -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()) diff --git a/src/containers/menu-container.tsx b/src/containers/menu-container.tsx index ad5cbc9..77fc78b 100644 --- a/src/containers/menu-container.tsx +++ b/src/containers/menu-container.tsx @@ -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) => { diff --git a/src/main/update-scripts.ts b/src/main/update-scripts.ts index 5a273bf..9ff974c 100644 --- a/src/main/update-scripts.ts +++ b/src/main/update-scripts.ts @@ -9,11 +9,7 @@ export default function performUpdate(store: Store) { 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) diff --git a/src/schema-types.ts b/src/schema-types.ts index 5fb2e92..88ef709 100644 --- a/src/schema-types.ts +++ b/src/schema-types.ts @@ -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 { diff --git a/src/scripts/i18n/en-US.json b/src/scripts/i18n/en-US.json index b1f1832..e226fe3 100644 --- a/src/scripts/i18n/en-US.json +++ b/src/scripts/i18n/en-US.json @@ -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", diff --git a/src/scripts/i18n/zh-CN.json b/src/scripts/i18n/zh-CN.json index 377c031..8fda473 100644 --- a/src/scripts/i18n/zh-CN.json +++ b/src/scripts/i18n/zh-CN.json @@ -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": "清理", diff --git a/src/scripts/models/app.ts b/src/scripts/models/app.ts index 3e79fa8..93c3db7 100644 --- a/src/scripts/models/app.ts +++ b/src/scripts/models/app.ts @@ -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 +} +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()) => ({ sids: sids, }) -export function exitSettings(): AppThunk { - return (dispatch, getState) => { +export function exitSettings(): AppThunk> { + 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() + 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) => { diff --git a/src/scripts/models/feed.ts b/src/scripts/models/feed.ts index d72b43e..762cfba 100644 --- a/src/scripts/models/feed.ts +++ b/src/scripts/models/feed.ts @@ -96,13 +96,13 @@ export class RSSFeed { this.filter = filter === null ? new FeedFilter() : filter } - static async loadFeed(feed: RSSFeed, init = false): Promise { + static async loadFeed(feed: RSSFeed, skip = 0): Promise { 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> { let promises = new Array>() 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> { - 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)) diff --git a/src/scripts/models/item.ts b/src/scripts/models/item.ts index 5e54cda..b1b4473 100644 --- a/src/scripts/models/item.ts +++ b/src/scripts/models/item.ts @@ -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 } } \ No newline at end of file diff --git a/src/scripts/models/service.ts b/src/scripts/models/service.ts index 90f8f2b..b7ff848 100644 --- a/src/scripts/models/service.ts +++ b/src/scripts/models/service.ts @@ -166,9 +166,6 @@ function syncItems(hook: ServiceHooks["syncItems"]): AppThunk> { 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)) - } } } } diff --git a/src/scripts/models/services/feedbin.ts b/src/scripts/models/services/feedbin.ts index 0be037a..6f07724 100644 --- a/src/scripts/models/services/feedbin.ts +++ b/src/scripts/models/services/feedbin.ts @@ -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 = new Set(await unreadResponse.json()) const starred: Set = new Set(await starredResponse.json()) - const parsedItems = items.map(i => { + const parsedItems = new Array() + 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 { diff --git a/src/scripts/models/source.ts b/src/scripts/models/source.ts index 88f5103..d93417d 100644 --- a/src/scripts/models/source.ts +++ b/src/scripts/models/source.ts @@ -181,8 +181,8 @@ export function initSources(): AppThunk> { source.unreadCount = 0 state[source.sid] = source } - await unreadCount(sources) - dispatch(initSourcesSuccess(sources)) + await unreadCount(state) + dispatch(initSourcesSuccess(state)) } }