diff --git a/src/bridges/settings.ts b/src/bridges/settings.ts index 3cc1640..7a09362 100644 --- a/src/bridges/settings.ts +++ b/src/bridges/settings.ts @@ -16,6 +16,13 @@ const settingsBridge = { ipcRenderer.invoke("set-menu", state) }, + getIconStatus: (): boolean => { + return ipcRenderer.sendSync("get-icon-status") + }, + toggleIconStatus: () => { + ipcRenderer.send("toggle-icon-status") + }, + getProxyStatus: (): boolean => { return ipcRenderer.sendSync("get-proxy-status") }, diff --git a/src/components/settings/app.tsx b/src/components/settings/app.tsx index c8ecad3..aba04a2 100644 --- a/src/components/settings/app.tsx +++ b/src/components/settings/app.tsx @@ -3,7 +3,7 @@ import intl from "react-intl-universal" import { urlTest, byteToMB, calculateItemSize, getSearchEngineName } from "../../scripts/utils" import { ThemeSettings, SearchEngines } from "../../schema-types" import { getThemeSettings, setThemeSettings, exportAll } from "../../scripts/settings" -import { Stack, Label, Toggle, TextField, DefaultButton, ChoiceGroup, IChoiceGroupOption, loadTheme, Dropdown, IDropdownOption, PrimaryButton } from "@fluentui/react" +import { Stack, Label, Toggle, TextField, DefaultButton, ChoiceGroup, IChoiceGroupOption, loadTheme, Dropdown, IDropdownOption, PrimaryButton, ToggleBase } from "@fluentui/react" import DangerButton from "../utils/danger-button" type AppTabProps = { @@ -20,6 +20,7 @@ type AppTabState = { itemSize: string cacheSize: string deleteIndex: string + iconStatus: boolean } class AppTab extends React.Component { @@ -31,7 +32,8 @@ class AppTab extends React.Component { themeSettings: getThemeSettings(), itemSize: null, cacheSize: null, - deleteIndex: null + deleteIndex: null, + iconStatus: window.settings.getIconStatus() } this.getItemSize() this.getCacheSize() @@ -73,6 +75,11 @@ class AppTab extends React.Component { this.props.setFetchInterval(item.key as number) } + toggleIcon = () => { + window.settings.toggleIconStatus() + this.setState({ iconStatus: window.settings.getIconStatus() }) + } + searchEngineOptions = (): IDropdownOption[] => [ SearchEngines.Google, SearchEngines.Bing, SearchEngines.Baidu, SearchEngines.DuckDuckGo ].map(engine => ({ @@ -183,6 +190,13 @@ class AppTab extends React.Component { + + diff --git a/src/electron.ts b/src/electron.ts index 3ce11ae..c82abb7 100644 --- a/src/electron.ts +++ b/src/electron.ts @@ -16,6 +16,9 @@ else if (process.platform === "win32") app.setAppUserModelId("me.hyliu.fluentrea let restarting = false +// Usage of any user agent will promt youtube to redirect url, causing issues reading response +app.userAgentFallback = " " + function init() { performUpdate(store) nativeTheme.themeSource = store.get("theme", ThemeSettings.Default) diff --git a/src/main/settings.ts b/src/main/settings.ts index 50ab326..c358003 100644 --- a/src/main/settings.ts +++ b/src/main/settings.ts @@ -22,6 +22,15 @@ ipcMain.handle("set-menu", (_, state: boolean) => { store.set(MENU_STORE_KEY, state) }) + +const ICON_STATUS_KEY = "customIcon" +function getIconStatus() { + return store.get(ICON_STATUS_KEY, true) +} +function toggleIconStatus() { + store.set(ICON_STATUS_KEY, !getIconStatus()) +} + const PAC_STORE_KEY = "pac" const PAC_STATUS_KEY = "pacOn" function getProxyStatus() { @@ -46,6 +55,13 @@ function setProxy(address = null) { session.fromPartition("sandbox").setProxy(rules) } } +ipcMain.on("get-icon-status", (event) => { + event.returnValue = getIconStatus() +}) +ipcMain.on("toggle-icon-status", () => { + toggleIconStatus() +}) + ipcMain.on("get-proxy-status", (event) => { event.returnValue = getProxyStatus() }) diff --git a/src/schema-types.ts b/src/schema-types.ts index fc54ecf..2000bbb 100644 --- a/src/schema-types.ts +++ b/src/schema-types.ts @@ -78,4 +78,5 @@ export type SchemaTypes = { filterType: number listViewConfigs: ViewConfigs useNeDB: boolean + customIcon: boolean } diff --git a/src/scripts/models/item.ts b/src/scripts/models/item.ts index c373684..e706925 100644 --- a/src/scripts/models/item.ts +++ b/src/scripts/models/item.ts @@ -1,7 +1,7 @@ import * as db from "../db" import lf from "lovefield" import intl from "react-intl-universal" -import { domParser, htmlDecode, ActionStatus, AppThunk, platformCtrl } from "../utils" +import { domParser, htmlDecode, parseYouTubeContent, ActionStatus, AppThunk, platformCtrl } from "../utils" import { RSSSource, updateSource, updateUnreadCounts } from "./source" import { FeedActionTypes, INIT_FEED, LOAD_MORE, FilterType, initFeeds, dismissItems } from "./feed" import Parser from "@yang991178/rss-parser" @@ -75,6 +75,13 @@ export class RSSItem { if (item.thumb && !item.thumb.startsWith("https://") && !item.thumb.startsWith("http://")) { delete item.thumb } + if (parsed.videoMeta) { + item.thumb = item.thumb ? item.thumb : parsed.videoMeta["media:thumbnail"][0].$.url + item.snippet = item.snippet ? item.snippet : parsed.videoMeta["media:description"][0] || "" + const views = parsed.videoMeta["media:community"][0]["media:statistics"][0].$.views + const likes = parsed.videoMeta["media:community"][0]["media:starRating"][0].$.count + item.content = parseYouTubeContent(item.link.replace("watch?v=","embed/"), views, likes, item.snippet) + } } } diff --git a/src/scripts/models/source.ts b/src/scripts/models/source.ts index 9e1ead7..2dcc8aa 100644 --- a/src/scripts/models/source.ts +++ b/src/scripts/models/source.ts @@ -2,7 +2,7 @@ import Parser from "@yang991178/rss-parser" import intl from "react-intl-universal" import * as db from "../db" import lf from "lovefield" -import { fetchFavicon, ActionStatus, AppThunk, parseRSS } from "../utils" +import { fetchFavicon, fetchYTChannelIcon, ActionStatus, AppThunk, parseRSS } from "../utils" import { RSSItem, insertItems, ItemActionTypes, FETCH_ITEMS, MARK_READ, MARK_UNREAD, MARK_ALL_READ } from "./item" import { saveSettings } from "./app" import { SourceRule } from "./rule" @@ -322,8 +322,8 @@ export function updateFavicon(sids?: number[], force=false): AppThunk { const url = initSources[sid].url - let favicon = (await fetchFavicon(url)) || "" const source = getState().sources[sid] + let favicon = (await fetchFavicon(url)) || "" if (source && source.url === url && (force || source.iconurl === undefined)) { source.iconurl = favicon await dispatch(updateSource(source)) diff --git a/src/scripts/utils.ts b/src/scripts/utils.ts index e594aa7..1f1f690 100644 --- a/src/scripts/utils.ts +++ b/src/scripts/utils.ts @@ -24,6 +24,7 @@ const rssParser = new Parser({ item: [ "thumb", "image", ["content:encoded", "fullContent"], ['media:content', 'mediaContent', {keepArray: true}], + ['media:group', 'videoMeta', {keepArray: false}], ] as Parser.CustomFieldItem[] } }) @@ -73,8 +74,33 @@ export async function parseRSS(url: string) { export const domParser = new DOMParser() +export async function fetchYTChannelIcon(url: string) { + let channelId = url.split("=")[1] + url = url.split("/").slice(0, 3).join("/") + let channelUrl = url + '/channel/' + channelId + let result = await fetch(channelUrl, { credentials: 'omit', }) + if (result.ok) { + let html = await result.text() + let dom = domParser.parseFromString(html, "text/html") + let links = dom.getElementsByTagName("link") + for (let link of links) { + let rel = link.getAttribute("rel") + if (rel === "image_src" && link.hasAttribute("href")) { + let href = link.getAttribute("href") + return href.replace("=s900", "=s16") + } + } + } +} + export async function fetchFavicon(url: string) { try { + const customIcon = window.settings.getIconStatus() + if (customIcon) { + if (Url.parse(url).host === "www.youtube.com" && url.includes("channel")) { + return fetchYTChannelIcon(url) + } + } url = url.split("/").slice(0, 3).join("/") let result = await fetch(url, { credentials: "omit" }) if (result.ok) { @@ -88,6 +114,7 @@ export async function fetchFavicon(url: string) { let parsedUrl = Url.parse(url) if (href.startsWith("//")) return parsedUrl.protocol + href else if (href.startsWith("/")) return url + href + else if (href.startsWith("favicon")) return url + '/' + href else return href } } @@ -121,6 +148,13 @@ export function htmlDecode(input: string) { return doc.documentElement.textContent } +export function parseYouTubeContent(url: string, views: string, likes: string, content: string){ + const video = `` + const views_likes = `

${views} views

${likes} likes

` + const _content = `
${content.replace(/(?:\r\n|\r|\n)/g, '
')}
` + return `${video}${views_likes}



${_content}` +} + export const urlTest = (s: string) => /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,63}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/gi.test(s)