diff --git a/src/bridges/settings.ts b/src/bridges/settings.ts index 0c710b95..905d0d58 100644 --- a/src/bridges/settings.ts +++ b/src/bridges/settings.ts @@ -53,11 +53,11 @@ const settingsBridge = { }); }, - getAnimationMotionPref: (): AnimationMotionPref => { - return ipcRenderer.sendSync("get-animation-motion-pref"); + getAnimationMotionPref: (): Promise => { + return ipcRenderer.invoke("get-animation-motion-pref"); }, - setAnimationMotionPref: (pref: AnimationMotionPref) => { - ipcRenderer.invoke("set-animation-motion-pref", pref); + setAnimationMotionPref: (pref: AnimationMotionPref): Promise => { + return ipcRenderer.invoke("set-animation-motion-pref", pref); }, getNativeWindowFramePref: (): boolean => { @@ -67,11 +67,11 @@ const settingsBridge = { ipcRenderer.invoke("set-window-native-frame-pref", pref); }, - getThumbnailTypePref: (): ThumbnailTypePref => { - return ipcRenderer.sendSync("get-thumbnail-type-pref"); + getThumbnailTypePref: (): Promise => { + return ipcRenderer.invoke("get-thumbnail-type-pref"); }, - setThumbnailTypePref: (pref: ThumbnailTypePref) => { - ipcRenderer.invoke("set-thumbnail-type-pref", pref); + setThumbnailTypePref: (pref: ThumbnailTypePref): Promise => { + return ipcRenderer.invoke("set-thumbnail-type-pref", pref); }, setLocaleSettings: (option: string) => { diff --git a/src/components/cards/cached-img.tsx b/src/components/cards/cached-img.tsx index 2c022065..6e7a1885 100644 --- a/src/components/cards/cached-img.tsx +++ b/src/components/cards/cached-img.tsx @@ -1,9 +1,9 @@ import * as React from "react"; import { AnimationMotionPref } from "../../schema-types"; -import { getAnimationMotionPref } from "../../scripts/settings"; type ImgProps = { src: string; + animationPref: AnimationMotionPref; className?: string; width?: number; height?: number; @@ -22,12 +22,10 @@ class CachedImg extends React.Component { | PlaceholderImageSource >(); private readonly _canvasRef = React.createRef(); + private _animationMotionPref: AnimationMotionPref; private _needsRescaling: boolean = true; private _imgSource: - | HTMLImageElement - | HTMLVideoElement - | VideoFrame[] - | null = null; + HTMLImageElement | HTMLVideoElement | VideoFrame[] | null = null; private _animationTimeout: NodeJS.Timeout | null = null; private _requestVideoFrameCallback: number | null = null; private _maxCanvasWidth: number; @@ -36,6 +34,7 @@ class CachedImg extends React.Component { constructor(props: ImgProps) { super(props); + this._animationMotionPref = props.animationPref; this._maxCanvasWidth = props.width ?? 512; this._maxCanvasHeight = props.height ?? 512; } @@ -143,14 +142,6 @@ class CachedImg extends React.Component { } private async renderImage(): Promise { - const animationMotionPref = getAnimationMotionPref(); - const realisedAnimationMotionPref = - animationMotionPref !== AnimationMotionPref.System - ? animationMotionPref - : window.utils.systemPreferencesGetAnimationSettings() - .prefersReducedMotion - ? AnimationMotionPref.Off - : AnimationMotionPref.On; if (CachedImg._cache.has(this.props.src)) { const imgSource = CachedImg._cache.get(this.props.src); if (imgSource === placeholderImageSource) { @@ -215,47 +206,63 @@ class CachedImg extends React.Component { this.forceUpdate(); return; } - } else if (this._imgSource instanceof HTMLVideoElement) { - const video = this._imgSource; - if (this._requestVideoFrameCallback !== null) { - video.cancelVideoFrameCallback( - this._requestVideoFrameCallback, - ); - this._requestVideoFrameCallback = null; - } - const requestFrame = () => { - this.draw(video); - if (realisedAnimationMotionPref === AnimationMotionPref.On) - this._requestVideoFrameCallback = - video.requestVideoFrameCallback(requestFrame); - }; - if (video.readyState < video.HAVE_CURRENT_DATA) - video.addEventListener("loadeddata", requestFrame); - else requestFrame(); } else { - const frames = this._imgSource; - let frameIndex = 0; - if (this._animationTimeout) { - clearTimeout(this._animationTimeout); - this._animationTimeout = null; - } - const drawFrame = () => { - if (frames.length === 0) return; - const frame = frames[frameIndex]; - const duration = frame.duration / 1000; - this.draw(frame); - if ( - frames.length > 1 && - realisedAnimationMotionPref === AnimationMotionPref.On - ) { - frameIndex = (frameIndex + 1) % frames.length; - this._animationTimeout = setTimeout( - () => drawFrame(), - duration, + const realisedAmp = + this._animationMotionPref !== AnimationMotionPref.System + ? this._animationMotionPref + : window.utils.systemPreferencesGetAnimationSettings() + .prefersReducedMotion + ? AnimationMotionPref.Off + : AnimationMotionPref.On; + + if (this._imgSource instanceof HTMLVideoElement) { + const video = this._imgSource; + if (this._requestVideoFrameCallback !== null) { + video.cancelVideoFrameCallback( + this._requestVideoFrameCallback, ); + this._requestVideoFrameCallback = null; } - }; - drawFrame(); + const requestFrame = async () => { + this.draw(video); + const realisedAnimationMotionPref = realisedAmp; + if ( + realisedAnimationMotionPref === + AnimationMotionPref.On + ) + this._requestVideoFrameCallback = + video.requestVideoFrameCallback(requestFrame); + }; + if (video.readyState < video.HAVE_CURRENT_DATA) + video.addEventListener("loadeddata", requestFrame); + else await requestFrame(); + } else { + const frames = this._imgSource; + let frameIndex = 0; + if (this._animationTimeout) { + clearTimeout(this._animationTimeout); + this._animationTimeout = null; + } + const drawFrame = async () => { + if (frames.length === 0) return; + const frame = frames[frameIndex]; + const duration = frame.duration / 1000; + this.draw(frame); + const realisedAnimationMotionPref = realisedAmp; + if ( + frames.length > 1 && + realisedAnimationMotionPref === + AnimationMotionPref.On + ) { + frameIndex = (frameIndex + 1) % frames.length; + this._animationTimeout = setTimeout( + () => drawFrame(), + duration, + ); + } + }; + await drawFrame(); + } } } } diff --git a/src/components/cards/thumbnail.tsx b/src/components/cards/thumbnail.tsx index 876725c8..8583eb70 100644 --- a/src/components/cards/thumbnail.tsx +++ b/src/components/cards/thumbnail.tsx @@ -1,7 +1,11 @@ import * as React from "react"; import { RSSItem } from "../../scripts/models/item"; import CachedImg from "./cached-img"; -import { getThumbnailTypePref } from "../../scripts/settings"; +import { + type AnimationMotionPref, + type ThumbnailTypePref, +} from "../../schema-types"; +import { type RootState, useAppSelector } from "../../scripts/reducer"; type CardThumbnailProps = { item: RSSItem; @@ -12,6 +16,7 @@ type CardThumbnailProps = { const mediaElement = ( src: string, + animationPref: AnimationMotionPref, className: string, width: number | null, height: number | null, @@ -19,14 +24,27 @@ const mediaElement = ( return ( ); }; +/** Selector for animation motion preferences. */ +function useAnimationMotionPref(state: RootState) { + return state.app.animationMotionPref; +} + +/** Selector for the thumbnail preference. */ +function useThumbnailTypePref(state: RootState): ThumbnailTypePref { + return state.app.thumbnailTypePref; +} + function CardThumbnail(props: CardThumbnailProps): React.JSX.Element { - const preferredThumbnailType = getThumbnailTypePref(); + const preferredThumbnailType = useAppSelector(useThumbnailTypePref); + const animationMotionPref = useAppSelector(useAnimationMotionPref); + const preferredThumbnail = props.item.thumbnails?.filter( (t) => t.type === preferredThumbnailType, )?.[0]; @@ -34,6 +52,7 @@ function CardThumbnail(props: CardThumbnailProps): React.JSX.Element { return selectedThumbnail ? mediaElement( new URL(selectedThumbnail.url, props.item.link).toString(), + animationMotionPref, props.className, props.width, props.height, diff --git a/src/components/settings/app.tsx b/src/components/settings/app.tsx index b58485aa..43118963 100644 --- a/src/components/settings/app.tsx +++ b/src/components/settings/app.tsx @@ -18,15 +18,16 @@ import { getThemeSettings, setThemeSettings, getAnimationMotionPref, - setAnimationMotionPref, setDefaultOpenTargetPref, getNativeWindowFramePref, setNativeWindowFramePref, exportAll, - getThumbnailTypePref, - setThumbnailTypePref, } from "../../scripts/settings"; -import { SET_DEFAULT_OPEN_TARGET } from "../../scripts/models/app"; +import { + SET_DEFAULT_OPEN_TARGET, + setThumbnailPref, + setAnimationPref, +} from "../../scripts/models/app"; import { ChoiceGroup, DefaultButton, @@ -382,13 +383,18 @@ function PacSettings() { ); } +/** Selector for the animation preference. */ +function useAnimationMotionPref(state: RootState): AnimationMotionPref { + return state.app.animationMotionPref; +} + /** * React component for animation preference dropdown. */ function AnimationPreferences(): React.JSX.Element { - const [animationProp, setAnimationProp] = React.useState( - getAnimationMotionPref(), - ); + const dispatch = useAppDispatch(); + const animationPref = useAppSelector(useAnimationMotionPref); + const preferenceOptions: IDropdownOption[] = [ { key: AnimationMotionPref.System, @@ -408,8 +414,7 @@ function AnimationPreferences(): React.JSX.Element { }, ]; const prefChange = (_: any, item: IDropdownOption) => { - setAnimationMotionPref(item.key as AnimationMotionPref); - setAnimationProp(item.key as AnimationMotionPref); + dispatch(setAnimationPref(item.key as AnimationMotionPref)); }; return ( @@ -421,7 +426,7 @@ function AnimationPreferences(): React.JSX.Element { @@ -459,10 +464,14 @@ function NativeWindowFramePreference(): React.JSX.Element { ); } +/** Selector for the thumbnail preference. */ +function useThumbnailTypePref(state: RootState): ThumbnailTypePref { + return state.app.thumbnailTypePref; +} + function ThumbnailTypePreferences() { - const [thumbnailType, setThumbnailType] = React.useState( - getThumbnailTypePref(), - ); + const dispatch = useAppDispatch(); + const thumbnailType = useAppSelector(useThumbnailTypePref); const preferenceOptions: IDropdownOption[] = [ { key: ThumbnailTypePref.OpenGraph, @@ -484,8 +493,7 @@ function ThumbnailTypePreferences() { }, ]; const prefChange = (_: any, item: IDropdownOption) => { - setThumbnailTypePref(item.key as ThumbnailTypePref); - setThumbnailType(item.key as ThumbnailTypePref); + dispatch(setThumbnailPref(item.key as ThumbnailTypePref)); }; return ( diff --git a/src/main/settings.ts b/src/main/settings.ts index 30dce935..91bd5543 100644 --- a/src/main/settings.ts +++ b/src/main/settings.ts @@ -104,8 +104,8 @@ export function setThemeListener(manager: WindowManager) { } const ANIMATION_MOTION_PREF_KEY = "animationMotionPref"; -ipcMain.on("get-animation-motion-pref", (event) => { - event.returnValue = getStore().get( +ipcMain.handle("get-animation-motion-pref", () => { + return getStore().get( ANIMATION_MOTION_PREF_KEY, AnimationMotionPref.System, ); @@ -126,11 +126,8 @@ ipcMain.handle("set-window-native-frame-pref", (_, pref: boolean) => { }); const THUMBNAIL_TYPE_PREF = "thumbnailTypePref"; -ipcMain.on("get-thumbnail-type-pref", (event) => { - event.returnValue = getStore().get( - THUMBNAIL_TYPE_PREF, - ThumbnailTypePref.OpenGraph, - ); +ipcMain.handle("get-thumbnail-type-pref", () => { + return getStore().get(THUMBNAIL_TYPE_PREF, ThumbnailTypePref.OpenGraph); }); ipcMain.handle("set-thumbnail-type-pref", (_, pref: ThumbnailTypePref) => { getStore().set(THUMBNAIL_TYPE_PREF, pref); diff --git a/src/scripts/models/app.ts b/src/scripts/models/app.ts index 8aca0d00..0dd8a41e 100644 --- a/src/scripts/models/app.ts +++ b/src/scripts/models/app.ts @@ -30,10 +30,16 @@ import { getCurrentLocale, setThemeDefaultFont, hasWindowFrame, + setAnimationMotionPref, + setThumbnailTypePref, } from "../settings"; import locales from "../i18n/_locales"; import { SYNC_SERVICE, ServiceActionTypes } from "./service"; -import { SourceOpenTarget } from "../../schema-types"; +import { + AnimationMotionPref, + SourceOpenTarget, + ThumbnailTypePref, +} from "../../schema-types"; export const enum ContextMenuType { Hidden, @@ -85,7 +91,11 @@ export class AppState { menu = getWindowBreakpoint() && window.settings.getDefaultMenu(); menuKey = ALL; title = ""; + defaultOpenTarget = SourceOpenTarget.Local; + animationMotionPref = AnimationMotionPref.System; + thumbnailTypePref = ThumbnailTypePref.OpenGraph; + addSourceModal = { display: false, }; @@ -196,6 +206,8 @@ export const SET_SETTINGS_TAB = "SET_SETTINGS_TAB"; export const SAVE_SETTINGS = "SAVE_SETTINGS"; export const FREE_MEMORY = "FREE_MEMORY"; export const SET_DEFAULT_OPEN_TARGET = "SET_DEFAULT_OPEN_TARGET"; +export const SET_THUMBNAIL_TYPE_PREF = "SET_THUMBNAIL_TYPE_PREF"; +export const SET_ANIMATION_MOTION_PREF = "SET_ANIMATION_MOTION_PREF"; interface ToggleSettingsAction { type: typeof TOGGLE_SETTINGS; @@ -218,12 +230,22 @@ interface SetDefaultOpenTargetAction { type: typeof SET_DEFAULT_OPEN_TARGET; value: SourceOpenTarget; } +interface SetThumbnailTypePref { + type: typeof SET_THUMBNAIL_TYPE_PREF; + value: ThumbnailTypePref; +} +interface SetAnimationMotionPref { + type: typeof SET_ANIMATION_MOTION_PREF; + value: AnimationMotionPref; +} export type SettingsActionTypes = - | ToggleSettingsAction + | FreeMemoryAction + | SaveSettingsAction + | SetAnimationMotionPref | SetDefaultOpenTargetAction | SetSettingsTabAction - | SaveSettingsAction - | FreeMemoryAction; + | SetThumbnailTypePref + | ToggleSettingsAction; const SHOW_ADD_SOURCE_MODAL = "SHOW_ADD_SOURCE_MODAL"; const HIDE_ADD_SOURCE_MODAL = "HIDE_ADD_SOURCE_MODAL"; @@ -236,8 +258,7 @@ interface HideAddSourceModalAction { } export type AddSourceModalActionTypes = - | ShowAddSourceModalAction - | HideAddSourceModalAction; + ShowAddSourceModalAction | HideAddSourceModalAction; export function showAddSourceModal(): ShowAddSourceModalAction { return { type: SHOW_ADD_SOURCE_MODAL }; @@ -247,6 +268,20 @@ export function hideAddSourceModal(): HideAddSourceModalAction { return { type: HIDE_ADD_SOURCE_MODAL }; } +export function setThumbnailPref( + pref: ThumbnailTypePref, +): SetThumbnailTypePref { + setThumbnailTypePref(pref); + return { type: SET_THUMBNAIL_TYPE_PREF, value: pref }; +} + +export function setAnimationPref( + pref: AnimationMotionPref, +): SetAnimationMotionPref { + setAnimationMotionPref(pref); + return { type: SET_ANIMATION_MOTION_PREF, value: pref }; +} + export function closeContextMenu(): AppThunk { return (dispatch, getState) => { if (getState().app.contextMenu.type !== ContextMenuType.Hidden) { @@ -453,6 +488,12 @@ export function initApp(): AppThunk { type: SET_DEFAULT_OPEN_TARGET, value: defaultOpenTarget, }); + const thumbnailPref = + await window.settings.getThumbnailTypePref(); + dispatch({ + type: SET_THUMBNAIL_TYPE_PREF, + value: thumbnailPref, + }); await dispatch(initSources()); }) .then(() => dispatch(initFeeds())) @@ -771,6 +812,16 @@ export function appReducer( ...state, defaultOpenTarget: action.value, }; + case SET_THUMBNAIL_TYPE_PREF: + return { + ...state, + thumbnailTypePref: action.value, + }; + case SET_ANIMATION_MOTION_PREF: + return { + ...state, + animationMotionPref: action.value, + }; case TOGGLE_LOGS: return { ...state, diff --git a/src/scripts/settings.ts b/src/scripts/settings.ts index 8f029e88..e1dfbde2 100644 --- a/src/scripts/settings.ts +++ b/src/scripts/settings.ts @@ -93,13 +93,13 @@ export function initThemeUpdateListener() { }); } -export function getThumbnailTypePref(): ThumbnailTypePref { +export async function getThumbnailTypePref(): Promise { return window.settings.getThumbnailTypePref(); } -export function setThumbnailTypePref(pref: ThumbnailTypePref) { - window.settings.setThumbnailTypePref(pref); +export function setThumbnailTypePref(pref: ThumbnailTypePref): Promise { + return window.settings.setThumbnailTypePref(pref); } -export function getAnimationMotionPref(): AnimationMotionPref { +export async function getAnimationMotionPref(): Promise { return window.settings.getAnimationMotionPref(); } export function setAnimationMotionPref(pref: AnimationMotionPref) { @@ -125,8 +125,8 @@ export function hasWindowFrame() { getNativeWindowFramePref() ); } -export function applyAnimationMotionPref() { - const pref = getAnimationMotionPref(); +export async function applyAnimationMotionPref(): Promise { + const pref = await getAnimationMotionPref(); let realisedPref = pref; if (pref === AnimationMotionPref.System) { const animationSettings =