From 7754b1eb4a46a2307d16f289826f780fdd448ace Mon Sep 17 00:00:00 2001 From: Alexander Harding <2166114+aeharding@users.noreply.github.com> Date: Sat, 27 Apr 2024 23:02:44 -0500 Subject: [PATCH] Add vote theme (options: lemmy, reddit) --- src/core/theme/variables.ts | 7 ++ src/features/labels/Vote.tsx | 31 +----- src/features/labels/VoteStat.tsx | 37 +++++++ src/features/post/shared/VoteButton.tsx | 13 ++- .../settings/appearance/themes/Theme.tsx | 2 + .../themes/votesTheme/VotesTheme.tsx | 102 ++++++++++++++++++ src/features/settings/settingsSlice.tsx | 10 ++ .../shared/sliding/BaseSlidingVote.tsx | 10 +- src/services/db.ts | 9 ++ 9 files changed, 190 insertions(+), 31 deletions(-) create mode 100644 src/features/labels/VoteStat.tsx create mode 100644 src/features/settings/appearance/themes/votesTheme/VotesTheme.tsx diff --git a/src/core/theme/variables.ts b/src/core/theme/variables.ts index 3eb17c9b61..9effe6c269 100644 --- a/src/core/theme/variables.ts +++ b/src/core/theme/variables.ts @@ -93,6 +93,8 @@ export const baseVariables = css` --read-color-medium: rgba(0, 0, 0, 0.4); --share-img-drop-shadow: none; + + --ion-color-reddit-upvote: #ff5c01; } .ios body { @@ -107,6 +109,9 @@ export const baseVariables = css` .ion-color-primary-fixed { --ion-color-base: var(--ion-color-primary-fixed); } + .ion-color-reddit-upvote { + --ion-color-base: var(--ion-color-reddit-upvote); + } } `; @@ -224,6 +229,8 @@ export const darkVariables = css` --read-color-medium: rgba(255, 255, 255, 0.4); --share-img-drop-shadow: drop-shadow(0 0 8px black); + + --ion-color-reddit-upvote: #f26700; } // iOS Dark Theme diff --git a/src/features/labels/Vote.tsx b/src/features/labels/Vote.tsx index 3c5c81ae4d..c5ce654fc5 100644 --- a/src/features/labels/Vote.tsx +++ b/src/features/labels/Vote.tsx @@ -16,11 +16,10 @@ import { ImpactStyle } from "@capacitor/haptics"; import useHapticFeedback from "../../helpers/useHapticFeedback"; import useAppToast from "../../helpers/useAppToast"; import { formatNumber } from "../../helpers/number"; -import { styled } from "@linaria/react"; import { getVoteErrorMessage } from "../../helpers/lemmyErrors"; import { PlainButton } from "../shared/PlainButton"; -import Stat from "../post/detail/Stat"; import { css } from "@linaria/core"; +import VoteStat from "./VoteStat"; const iconClass = css` // Vote icons are tall and narrow, but svg container is square. @@ -29,26 +28,6 @@ const iconClass = css` margin: 0 -2px; `; -const VoteStat = styled(Stat)<{ - vote?: 1 | -1 | 0; - voteRepresented?: 1 | -1; -}>` - && { - color: ${({ vote, voteRepresented }) => { - if (voteRepresented === undefined || vote === voteRepresented) { - switch (vote) { - case 1: - return "var(--ion-color-primary-fixed)"; - case -1: - return "var(--ion-color-danger)"; - } - } - - return "inherit"; - }}; - } -`; - interface VoteProps { item: PostView | CommentView; className?: string; @@ -120,7 +99,7 @@ export default function Vote({ icon={arrowUpSharp} className={className} iconClassName={iconClass} - vote={myVote} + currentVote={myVote} voteRepresented={1} onClick={async (e) => { await onVote(e, myVote === 1 ? 0 : 1); @@ -133,7 +112,7 @@ export default function Vote({ icon={arrowDownSharp} className={className} iconClassName={iconClass} - vote={myVote} + currentVote={myVote} voteRepresented={-1} onClick={async (e) => { await onVote(e, myVote === -1 ? 0 : -1); @@ -151,7 +130,7 @@ export default function Vote({ icon={myVote === -1 ? arrowDownSharp : arrowUpSharp} className={className} iconClassName={iconClass} - vote={myVote} + currentVote={myVote} onClick={async (e) => { await onVote(e, myVote ? 0 : 1); }} @@ -166,7 +145,7 @@ export default function Vote({ icon={myVote === -1 ? arrowDownSharp : arrowUpSharp} className={className} iconClassName={iconClass} - vote={myVote} + currentVote={myVote} onClick={async (e) => { await onVote(e, myVote ? 0 : 1); }} diff --git a/src/features/labels/VoteStat.tsx b/src/features/labels/VoteStat.tsx new file mode 100644 index 0000000000..2cf0282f76 --- /dev/null +++ b/src/features/labels/VoteStat.tsx @@ -0,0 +1,37 @@ +import { ComponentProps, useMemo } from "react"; +import { useAppSelector } from "../../store"; +import Stat from "../post/detail/Stat"; +import { + VOTE_COLORS, + bgColorToVariable, +} from "../settings/appearance/themes/votesTheme/VotesTheme"; + +interface VoteStatProps extends ComponentProps { + currentVote: 1 | -1 | 0; + voteRepresented?: 1 | -1 | 0; +} + +export default function VoteStat({ + currentVote, + voteRepresented, + ...props +}: VoteStatProps) { + const votesTheme = useAppSelector( + (state) => state.settings.appearance.votesTheme, + ); + + const color = useMemo(() => { + if (voteRepresented === undefined || currentVote === voteRepresented) { + switch (currentVote) { + case 1: + return bgColorToVariable(VOTE_COLORS.UPVOTE[votesTheme]); + case -1: + return bgColorToVariable(VOTE_COLORS.DOWNVOTE[votesTheme]); + } + } + + return "inherit"; + }, [currentVote, voteRepresented, votesTheme]); + + return ; +} diff --git a/src/features/post/shared/VoteButton.tsx b/src/features/post/shared/VoteButton.tsx index 62493ad37a..e62e970601 100644 --- a/src/features/post/shared/VoteButton.tsx +++ b/src/features/post/shared/VoteButton.tsx @@ -13,6 +13,10 @@ import useHapticFeedback from "../../../helpers/useHapticFeedback"; import useAppToast from "../../../helpers/useAppToast"; import { styled } from "@linaria/react"; import { getVoteErrorMessage } from "../../../helpers/lemmyErrors"; +import { + VOTE_COLORS, + bgColorToVariable, +} from "../../settings/appearance/themes/votesTheme/VotesTheme"; const InactiveItem = styled(ActionButton)` ${bounceAnimationOnTransition} @@ -36,6 +40,9 @@ export function VoteButton({ type, postId }: VoteButtonProps) { const vibrate = useHapticFeedback(); const { presentLoginIfNeeded } = useContext(PageContext); const downvoteAllowed = useAppSelector(isDownvoteEnabledSelector); + const votesTheme = useAppSelector( + (state) => state.settings.appearance.votesTheme, + ); const postVotesById = useAppSelector((state) => state.post.postVotesById); const myVote = postVotesById[postId]; @@ -64,10 +71,10 @@ export function VoteButton({ type, postId }: VoteButtonProps) { const activeColor = (() => { switch (type) { - case "down": - return "var(--ion-color-danger)"; case "up": - return "var(--ion-color-primary)"; + return bgColorToVariable(VOTE_COLORS.UPVOTE[votesTheme]); + case "down": + return bgColorToVariable(VOTE_COLORS.DOWNVOTE[votesTheme]); } })(); diff --git a/src/features/settings/appearance/themes/Theme.tsx b/src/features/settings/appearance/themes/Theme.tsx index b8591c3ff5..c509f1bde4 100644 --- a/src/features/settings/appearance/themes/Theme.tsx +++ b/src/features/settings/appearance/themes/Theme.tsx @@ -2,12 +2,14 @@ import AppTheme from "./appTheme/AppTheme"; import CommentsTheme from "./commentsTheme/CommentsTheme"; import Dark from "./dark/Dark"; import System from "./system/System"; +import VotesTheme from "./votesTheme/VotesTheme"; export default function Theme() { return ( <> + diff --git a/src/features/settings/appearance/themes/votesTheme/VotesTheme.tsx b/src/features/settings/appearance/themes/votesTheme/VotesTheme.tsx new file mode 100644 index 0000000000..03644a7bde --- /dev/null +++ b/src/features/settings/appearance/themes/votesTheme/VotesTheme.tsx @@ -0,0 +1,102 @@ +import { + IonIcon, + IonItem, + IonLabel, + IonList, + IonRadio, + IonRadioGroup, +} from "@ionic/react"; +import { ListHeader } from "../../../shared/formatting"; +import { OVotesThemeType } from "../../../../../services/db"; +import { styled } from "@linaria/react"; +import { css } from "@linaria/core"; +import { useAppDispatch, useAppSelector } from "../../../../../store"; +import { startCase } from "lodash"; +import { arrowDownOutline, arrowUpOutline } from "ionicons/icons"; +import { setVotesTheme } from "../../../settingsSlice"; + +export const VOTE_COLORS = { + UPVOTE: { + [OVotesThemeType.Lemmy]: "primary-fixed", + [OVotesThemeType.Reddit]: "reddit-upvote", + }, + DOWNVOTE: { + [OVotesThemeType.Lemmy]: "danger", + [OVotesThemeType.Reddit]: "primary-fixed", + }, +}; + +export function bgColorToVariable(bgColor: string): string { + return `var(--ion-color-${bgColor})`; +} + +const VotesContainer = styled.div` + display: flex; + align-items: center; + justify-content: space-between; +`; + +const Votes = styled.div` + display: flex; + gap: 6px; + margin: 0 6px; +`; + +const Vote = styled.div<{ bgColor: string }>` + display: flex; + align-items: center; + justify-content: center; + padding: 4px; + border-radius: 4px; + + background: ${({ bgColor }) => bgColorToVariable(bgColor)}; + color: white; +`; + +export default function VotesTheme() { + const dispatch = useAppDispatch(); + const votesTheme = useAppSelector( + (state) => state.settings.appearance.votesTheme, + ); + + return ( + <> + + Votes Theme + + { + dispatch(setVotesTheme(e.detail.value)); + }} + > + + {Object.entries(OVotesThemeType).map(([label, value]) => ( + + + + {startCase(label)} + + + + + + + + + + + + ))} + + + + ); +} diff --git a/src/features/settings/settingsSlice.tsx b/src/features/settings/settingsSlice.tsx index cd7109e117..9df68e2a32 100644 --- a/src/features/settings/settingsSlice.tsx +++ b/src/features/settings/settingsSlice.tsx @@ -39,6 +39,7 @@ import { AutoplayMediaType, OAutoplayMediaType, CommentsThemeType, + VotesThemeType, } from "../../services/db"; import { get, set } from "./storage"; import { Mode } from "@ionic/core"; @@ -95,6 +96,7 @@ interface SettingsState { deviceMode: Mode; theme: AppThemeType; commentsTheme: CommentsThemeType; + votesTheme: VotesThemeType; }; general: { comments: { @@ -189,6 +191,7 @@ export const initialState: SettingsState = { deviceMode: "ios", theme: "default", commentsTheme: "rainbow", + votesTheme: "lemmy", }, general: { comments: { @@ -492,6 +495,10 @@ export const appearanceSlice = createSlice({ state.appearance.commentsTheme = action.payload; db.setSetting("comments_theme", action.payload); }, + setVotesTheme(state, action: PayloadAction) { + state.appearance.votesTheme = action.payload; + db.setSetting("votes_theme", action.payload); + }, setEnableHapticFeedback(state, action: PayloadAction) { state.general.enableHapticFeedback = action.payload; @@ -609,6 +616,7 @@ export const fetchSettingsFromDatabase = createAsyncThunk( const result = db.transaction("r", db.settings, async () => { const state = thunkApi.getState() as RootState; const comments_theme = await db.getSetting("comments_theme"); + const votes_theme = await db.getSetting("votes_theme"); const collapse_comment_threads = await db.getSetting( "collapse_comment_threads", ); @@ -693,6 +701,7 @@ export const fetchSettingsFromDatabase = createAsyncThunk( ...state.settings.appearance, commentsTheme: comments_theme ?? initialState.appearance.commentsTheme, + votesTheme: votes_theme ?? initialState.appearance.votesTheme, general: { userInstanceUrlDisplay: user_instance_url_display ?? @@ -836,6 +845,7 @@ export const fetchSettingsFromDatabase = createAsyncThunk( export const { setCommentsTheme, + setVotesTheme, setDatabaseError, setFontSizeMultiplier, setUseSystemFontSize, diff --git a/src/features/shared/sliding/BaseSlidingVote.tsx b/src/features/shared/sliding/BaseSlidingVote.tsx index 45e3f50936..f4463bce4c 100644 --- a/src/features/shared/sliding/BaseSlidingVote.tsx +++ b/src/features/shared/sliding/BaseSlidingVote.tsx @@ -46,6 +46,7 @@ import { AppContext } from "../../auth/AppContext"; import { getCanModerate } from "../../moderation/useCanModerate"; import { isStubComment } from "../../comment/CommentHeader"; import { getVoteErrorMessage } from "../../../helpers/lemmyErrors"; +import { VOTE_COLORS } from "../../settings/appearance/themes/votesTheme/VotesTheme"; const StyledItemContainer = styled.div` --ion-item-border-color: transparent; @@ -111,6 +112,10 @@ function BaseSlidingVoteInternal({ const presentToast = useAppToast(); const dispatch = useAppDispatch(); + const votesTheme = useAppSelector( + (state) => state.settings.appearance.votesTheme, + ); + const postVotesById = useAppSelector((state) => state.post.postVotesById); const commentVotesById = useAppSelector( (state) => state.comment.commentVotesById, @@ -315,7 +320,7 @@ function BaseSlidingVoteInternal({ trigger: () => { onVote(currentVote === 1 ? 0 : 1); }, - bgColor: "primary-fixed", + bgColor: VOTE_COLORS.UPVOTE[votesTheme], slash: currentVote === 1, }, downvote: { @@ -323,7 +328,7 @@ function BaseSlidingVoteInternal({ trigger: () => { onVote(currentVote === -1 ? 0 : -1); }, - bgColor: "danger", + bgColor: VOTE_COLORS.DOWNVOTE[votesTheme], slash: currentVote === -1, }, reply: { @@ -352,6 +357,7 @@ function BaseSlidingVoteInternal({ markUnreadAction, onVote, shareTrigger, + votesTheme, ]); const startActions: ActionList = useMemo( diff --git a/src/services/db.ts b/src/services/db.ts index 4b20a2509f..499d0453b8 100644 --- a/src/services/db.ts +++ b/src/services/db.ts @@ -45,6 +45,14 @@ export const OCommentsThemeType = { export type CommentsThemeType = (typeof OCommentsThemeType)[keyof typeof OCommentsThemeType]; +export const OVotesThemeType = { + Lemmy: "lemmy", + Reddit: "reddit", +} as const; + +export type VotesThemeType = + (typeof OVotesThemeType)[keyof typeof OVotesThemeType]; + export const OPostAppearanceType = { Compact: "compact", Large: "large", @@ -283,6 +291,7 @@ type ProvidersData = RedgifsProvider; export type SettingValueTypes = { comments_theme: CommentsThemeType; + votes_theme: VotesThemeType; collapse_comment_threads: CommentThreadCollapse; user_instance_url_display: InstanceUrlDisplayMode; vote_display_mode: VoteDisplayMode;