From 0ce4ed91f6d79e89d4363f5cc9a34c86e6ffde42 Mon Sep 17 00:00:00 2001 From: ujiro99 Date: Mon, 12 May 2025 12:45:18 +0900 Subject: [PATCH 01/10] Add: Shows a review screen when the user executes commands 100 times. --- src/background_script.ts | 45 +++++++++++++++++++++++++-------- src/services/defaultSettings.ts | 1 + src/services/settings.ts | 12 +++++++++ src/types.ts | 1 + 4 files changed, 49 insertions(+), 10 deletions(-) diff --git a/src/background_script.ts b/src/background_script.ts index de8051ae..bd8dface 100644 --- a/src/background_script.ts +++ b/src/background_script.ts @@ -19,9 +19,29 @@ import type { import { Storage, SESSION_STORAGE_KEY } from './services/storage' const OPTION_PAGE = 'src/options_page.html' +const REVIEW_THRESHOLD = 100 +const SETTING_KEY = { + COMMAND_EXECUTION_COUNT: 'commandExecutionCount', +} as const BgData.init() +// Increment command execution count +const incrementCommandExecutionCount = async () => { + await Settings.update(SETTING_KEY.COMMAND_EXECUTION_COUNT, (count) => (count ?? 0) + 1) +} + +// Show review request when threshold is reached +const showReviewRequest = async () => { + const settings = await Settings.get() + const count = settings.commandExecutionCount ?? 0 + if (count >= REVIEW_THRESHOLD) { + chrome.tabs.create({ + url: 'https://chromewebstore.google.com/detail/nlnhbibaommoelemmdfnkjkgoppkohje/reviews', + }) + } +} + type Sender = chrome.runtime.MessageSender export type openPopupAndClickProps = OpenPopupsProps & { @@ -68,24 +88,29 @@ function bindVariables( } const commandFuncs = { - [BgCommand.openPopups]: (param: OpenPopupsProps): boolean => { - openPopups(param) - return false + [BgCommand.openPopups]: (param: OpenPopupsProps, _: Sender, response: (res: unknown) => void): boolean => { + incrementCommandExecutionCount().then(async () => { + await openPopups(param) + await showReviewRequest() + response(true) + }) + return true }, - [BgCommand.openPopupAndClick]: (param: openPopupAndClickProps): boolean => { - const open = async () => { + [BgCommand.openPopupAndClick]: (param: openPopupAndClickProps, _: Sender, response: (res: unknown) => void): boolean => { + incrementCommandExecutionCount().then(async () => { const tabIds = await openPopups(param) if (tabIds.length > 0) { await Ipc.sendQueue(tabIds[0], TabCommand.clickElement, { selector: (param as { selector: string }).selector, }) - return + } else { + console.debug('tab not found') } - console.debug('tab not found') - } - open() - return false + await showReviewRequest() + response(true) + }) + return true }, [BgCommand.openOption]: (): boolean => { diff --git a/src/services/defaultSettings.ts b/src/services/defaultSettings.ts index 92533b1e..9f616860 100644 --- a/src/services/defaultSettings.ts +++ b/src/services/defaultSettings.ts @@ -87,6 +87,7 @@ export default { }, ], stars: [], + commandExecutionCount: 0, } as SettingsType export const PopupOption = { diff --git a/src/services/settings.ts b/src/services/settings.ts index c86d2b64..a2674fa1 100644 --- a/src/services/settings.ts +++ b/src/services/settings.ts @@ -129,6 +129,18 @@ export const Settings = { return true }, + update: async ( + key: T, + updater: (value: SettingsType[T]) => SettingsType[T], + ): Promise => { + const settings = await Settings.get() + const updatedSettings = { + ...settings, + [key]: updater(settings[key]), + } + return Settings.set(updatedSettings) + }, + addCommands: async (commands: Command[]): Promise => { const current = await Storage.getCommands() const newCommands = [...current, ...commands] diff --git a/src/types.ts b/src/types.ts index 9936e951..7154adb1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -140,6 +140,7 @@ export type SettingsType = { style: STYLE userStyles: Array stars: Array + commandExecutionCount?: number } export type SessionData = { From 650c1f248eaac6a3d83501bea9497e1cd76a4b22 Mon Sep 17 00:00:00 2001 From: ujiro99 Date: Mon, 12 May 2025 12:55:23 +0900 Subject: [PATCH 02/10] Update: If you have already requested a review, make sure not to request it again to avoid duplication. --- src/background_script.ts | 83 ++++++++++++++++++++++----------- src/services/defaultSettings.ts | 1 + src/types.ts | 1 + 3 files changed, 57 insertions(+), 28 deletions(-) diff --git a/src/background_script.ts b/src/background_script.ts index bd8dface..7a188391 100644 --- a/src/background_script.ts +++ b/src/background_script.ts @@ -18,27 +18,44 @@ import type { } from '@/types' import { Storage, SESSION_STORAGE_KEY } from './services/storage' -const OPTION_PAGE = 'src/options_page.html' -const REVIEW_THRESHOLD = 100 -const SETTING_KEY = { - COMMAND_EXECUTION_COUNT: 'commandExecutionCount', +const CONSTANTS = { + OPTION_PAGE: 'src/options_page.html', + REVIEW_THRESHOLD: 100, + SETTING_KEY: { + COMMAND_EXECUTION_COUNT: 'commandExecutionCount', + HAS_SHOWN_REVIEW_REQUEST: 'hasShownReviewRequest', + }, + URL: { + REVIEW: 'https://chromewebstore.google.com/detail/nlnhbibaommoelemmdfnkjkgoppkohje/reviews', + }, } as const BgData.init() // Increment command execution count -const incrementCommandExecutionCount = async () => { - await Settings.update(SETTING_KEY.COMMAND_EXECUTION_COUNT, (count) => (count ?? 0) + 1) +const incrementCommandExecutionCount = async (): Promise => { + try { + await Settings.update(CONSTANTS.SETTING_KEY.COMMAND_EXECUTION_COUNT, (count) => (count ?? 0) + 1) + } catch (error) { + console.error('Failed to increment command execution count:', error) + } } // Show review request when threshold is reached -const showReviewRequest = async () => { - const settings = await Settings.get() - const count = settings.commandExecutionCount ?? 0 - if (count >= REVIEW_THRESHOLD) { - chrome.tabs.create({ - url: 'https://chromewebstore.google.com/detail/nlnhbibaommoelemmdfnkjkgoppkohje/reviews', - }) +const showReviewRequest = async (): Promise => { + try { + const settings = await Settings.get() + const count = settings.commandExecutionCount ?? 0 + const hasShown = settings.hasShownReviewRequest ?? false + + if (count >= CONSTANTS.REVIEW_THRESHOLD && !hasShown) { + await Settings.update(CONSTANTS.SETTING_KEY.HAS_SHOWN_REVIEW_REQUEST, () => true) + chrome.tabs.create({ + url: CONSTANTS.URL.REVIEW, + }) + } + } catch (error) { + console.error('Failed to show review request:', error) } } @@ -90,32 +107,42 @@ function bindVariables( const commandFuncs = { [BgCommand.openPopups]: (param: OpenPopupsProps, _: Sender, response: (res: unknown) => void): boolean => { incrementCommandExecutionCount().then(async () => { - await openPopups(param) - await showReviewRequest() - response(true) + try { + await openPopups(param) + await showReviewRequest() + response(true) + } catch (error) { + console.error('Failed to execute openPopups:', error) + response(false) + } }) return true }, [BgCommand.openPopupAndClick]: (param: openPopupAndClickProps, _: Sender, response: (res: unknown) => void): boolean => { incrementCommandExecutionCount().then(async () => { - const tabIds = await openPopups(param) - if (tabIds.length > 0) { - await Ipc.sendQueue(tabIds[0], TabCommand.clickElement, { - selector: (param as { selector: string }).selector, - }) - } else { - console.debug('tab not found') + try { + const tabIds = await openPopups(param) + if (tabIds.length > 0) { + await Ipc.sendQueue(tabIds[0], TabCommand.clickElement, { + selector: (param as { selector: string }).selector, + }) + } else { + console.debug('tab not found') + } + await showReviewRequest() + response(true) + } catch (error) { + console.error('Failed to execute openPopupAndClick:', error) + response(false) } - await showReviewRequest() - response(true) }) return true }, [BgCommand.openOption]: (): boolean => { chrome.tabs.create({ - url: OPTION_PAGE, + url: CONSTANTS.OPTION_PAGE, }) return false }, @@ -137,7 +164,7 @@ const commandFuncs = { pageRules, }) chrome.tabs.create({ - url: `${OPTION_PAGE}#pageRules`, + url: `${CONSTANTS.OPTION_PAGE}#pageRules`, }) } add() @@ -414,7 +441,7 @@ const updateWindowSize = async ( chrome.action.onClicked.addListener(() => { chrome.tabs.create({ - url: OPTION_PAGE, + url: CONSTANTS.OPTION_PAGE, }) }) diff --git a/src/services/defaultSettings.ts b/src/services/defaultSettings.ts index 9f616860..23b21855 100644 --- a/src/services/defaultSettings.ts +++ b/src/services/defaultSettings.ts @@ -88,6 +88,7 @@ export default { ], stars: [], commandExecutionCount: 0, + hasShownReviewRequest: false, } as SettingsType export const PopupOption = { diff --git a/src/types.ts b/src/types.ts index 7154adb1..60a5f2a1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -141,6 +141,7 @@ export type SettingsType = { userStyles: Array stars: Array commandExecutionCount?: number + hasShownReviewRequest?: boolean } export type SessionData = { From 09752de0af4208f4fe3d9c4053cee4a819138f74 Mon Sep 17 00:00:00 2001 From: ujiro99 Date: Mon, 12 May 2025 21:57:48 +0900 Subject: [PATCH 03/10] Update: Fixed to maintain certain values when resetting/importing settings. --- src/components/option/ImportExport.tsx | 3 ++- src/services/settings.ts | 11 ++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/option/ImportExport.tsx b/src/components/option/ImportExport.tsx index e37c15ce..8a431935 100644 --- a/src/components/option/ImportExport.tsx +++ b/src/components/option/ImportExport.tsx @@ -84,8 +84,9 @@ export function ImportExport() { const handleImportClose = (ret: boolean) => { if (ret && importJson != null) { ; (async () => { + const preservedValues = await Settings.getPreservedValues() const data = await migrate(importJson) - await Settings.set(data) + await Settings.set({ ...data, ...preservedValues }) location.reload() })() } diff --git a/src/services/settings.ts b/src/services/settings.ts index a2674fa1..512ce0be 100644 --- a/src/services/settings.ts +++ b/src/services/settings.ts @@ -154,7 +154,8 @@ export const Settings = { }, reset: async () => { - await Storage.set(STORAGE_KEY.USER, DefaultSettings) + const preservedValues = await Settings.getPreservedValues() + await Storage.set(STORAGE_KEY.USER, { ...DefaultSettings, ...preservedValues }) await Storage.setCommands(DefaultCommands) }, @@ -171,6 +172,14 @@ export const Settings = { return Storage.get(LOCAL_STORAGE_KEY.CACHES) }, + getPreservedValues: async (): Promise> => { + const currentSettings = await Settings.get() + return { + commandExecutionCount: currentSettings.commandExecutionCount, + hasShownReviewRequest: currentSettings.hasShownReviewRequest, + } + }, + getUrls: (settings: SettingsType): string[] => { const iconUrls = settings.commands.map((c) => c.iconUrl) const folderIconUrls = settings.folders.map((f) => f.iconUrl) From b975afe42040d92b29f82c32e0e0ff4de2f9afcd Mon Sep 17 00:00:00 2001 From: ujiro99 Date: Tue, 13 May 2025 12:36:49 +0900 Subject: [PATCH 04/10] Update: Add shadcn/ui Sonner. --- package.json | 2 ++ src/components/ui/sonner.tsx | 31 +++++++++++++++++++++++++++++++ yarn.lock | 10 ++++++++++ 3 files changed, 43 insertions(+) create mode 100644 src/components/ui/sonner.tsx diff --git a/package.json b/package.json index 9a9c47c0..3591bdbe 100644 --- a/package.json +++ b/package.json @@ -36,12 +36,14 @@ "get-xpath": "^3.3.0", "lottie-web": "^5.12.2", "lucide-react": "^0.483.0", + "next-themes": "^0.4.6", "platform": "^1.3.6", "react": "^18.3.1", "react-dom": "^18.3.1", "react-hook-form": "^7.54.2", "react-textarea-autosize": "^8.5.3", "react-transition-group": "^4.4.5", + "sonner": "^2.0.3", "tailwind-merge": "^2.5.4", "tailwindcss-animate": "^1.0.7", "zod": "^3.24.1" diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx new file mode 100644 index 00000000..a8d15df3 --- /dev/null +++ b/src/components/ui/sonner.tsx @@ -0,0 +1,31 @@ +"use client" + +import { useTheme } from "next-themes" +import { Toaster as Sonner } from "sonner" + +type ToasterProps = React.ComponentProps + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme() + + return ( + + ) +} + +export { Toaster } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 8cbb4d25..31d09c48 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3535,6 +3535,11 @@ ndarray@^1.0.19: iota-array "^1.0.0" is-buffer "^1.0.2" +next-themes@^0.4.6: + version "0.4.6" + resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.4.6.tgz#8d7e92d03b8fea6582892a50a928c9b23502e8b6" + integrity sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA== + node-releases@^2.0.19: version "2.0.19" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" @@ -4368,6 +4373,11 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +sonner@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/sonner/-/sonner-2.0.3.tgz#de7cdbc4b6a25ac3f0a9e0aed3748e0b3d6e092e" + integrity sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA== + source-map-js@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" From c41ce9dbe97fa447ffd005b975bd0b1609ce64bd Mon Sep 17 00:00:00 2001 From: ujiro99 Date: Tue, 13 May 2025 22:28:37 +0900 Subject: [PATCH 05/10] Add: Toast for requesting user review. --- public/_locales/en/messages.json | 12 +++++++ public/_locales/ja/messages.json | 12 +++++++ src/background_script.ts | 46 +++++++++++++------------ src/components/App.tsx | 18 ++++++++++ src/components/ReviewRequestToast.tsx | 48 +++++++++++++++++++++++++++ src/services/ipc.ts | 1 + 6 files changed, 115 insertions(+), 22 deletions(-) create mode 100644 src/components/ReviewRequestToast.tsx diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json index 0854b3d7..1713afc6 100644 --- a/public/_locales/en/messages.json +++ b/public/_locales/en/messages.json @@ -721,5 +721,17 @@ }, "help_pageAction_share_desc": { "message": "You can share and get Page Action commands from the Selection Command Hub." + }, + "review_request_title": { + "message": "Enjoying Selection Command?" + }, + "review_request_message": { + "message": "If you find this extension helpful, please consider leaving a review on the Chrome Web Store. Your feedback helps us improve!" + }, + "review_request_button": { + "message": "Write a Review" + }, + "review_request_close": { + "message": "Not Now" } } diff --git a/public/_locales/ja/messages.json b/public/_locales/ja/messages.json index a69747db..44091ddb 100644 --- a/public/_locales/ja/messages.json +++ b/public/_locales/ja/messages.json @@ -718,5 +718,17 @@ }, "help_pageAction_share_desc": { "message": "Selection Command Hubから、Page Actionコマンドの共有と取得ができます。 " + }, + "review_request_title": { + "message": "Selection Commandをお楽しみいただいていますか?" + }, + "review_request_message": { + "message": "この拡張機能がお役に立っていると感じていただけましたら、Chrome Web Storeでのレビューをお願いいたします。皆様のフィードバックが改善の糧となります!" + }, + "review_request_button": { + "message": "レビューを書く" + }, + "review_request_close": { + "message": "後で" } } diff --git a/src/background_script.ts b/src/background_script.ts index 7a188391..e1db8cdd 100644 --- a/src/background_script.ts +++ b/src/background_script.ts @@ -25,37 +25,33 @@ const CONSTANTS = { COMMAND_EXECUTION_COUNT: 'commandExecutionCount', HAS_SHOWN_REVIEW_REQUEST: 'hasShownReviewRequest', }, - URL: { - REVIEW: 'https://chromewebstore.google.com/detail/nlnhbibaommoelemmdfnkjkgoppkohje/reviews', - }, + COMMAND_COUNT_THRESHOLD: 10, } as const BgData.init() -// Increment command execution count +// Increment command execution count and check review request const incrementCommandExecutionCount = async (): Promise => { - try { - await Settings.update(CONSTANTS.SETTING_KEY.COMMAND_EXECUTION_COUNT, (count) => (count ?? 0) + 1) - } catch (error) { - console.error('Failed to increment command execution count:', error) - } -} - -// Show review request when threshold is reached -const showReviewRequest = async (): Promise => { try { const settings = await Settings.get() const count = settings.commandExecutionCount ?? 0 const hasShown = settings.hasShownReviewRequest ?? false + // コマンド実行回数をカウントアップ + await Settings.update( + CONSTANTS.SETTING_KEY.COMMAND_EXECUTION_COUNT, + () => count + 1, + ) + + // 閾値を超えたらレビュー依頼を表示 if (count >= CONSTANTS.REVIEW_THRESHOLD && !hasShown) { - await Settings.update(CONSTANTS.SETTING_KEY.HAS_SHOWN_REVIEW_REQUEST, () => true) - chrome.tabs.create({ - url: CONSTANTS.URL.REVIEW, - }) + const tabs = await chrome.tabs.query({ active: true }) + if (tabs[0]?.id) { + await Ipc.sendTab(tabs[0].id, TabCommand.showReviewRequest) + } } } catch (error) { - console.error('Failed to show review request:', error) + console.error('Failed to increment command execution count:', error) } } @@ -105,11 +101,14 @@ function bindVariables( } const commandFuncs = { - [BgCommand.openPopups]: (param: OpenPopupsProps, _: Sender, response: (res: unknown) => void): boolean => { + [BgCommand.openPopups]: ( + param: OpenPopupsProps, + _: Sender, + response: (res: unknown) => void, + ): boolean => { incrementCommandExecutionCount().then(async () => { try { await openPopups(param) - await showReviewRequest() response(true) } catch (error) { console.error('Failed to execute openPopups:', error) @@ -119,7 +118,11 @@ const commandFuncs = { return true }, - [BgCommand.openPopupAndClick]: (param: openPopupAndClickProps, _: Sender, response: (res: unknown) => void): boolean => { + [BgCommand.openPopupAndClick]: ( + param: openPopupAndClickProps, + _: Sender, + response: (res: unknown) => void, + ): boolean => { incrementCommandExecutionCount().then(async () => { try { const tabIds = await openPopups(param) @@ -130,7 +133,6 @@ const commandFuncs = { } else { console.debug('tab not found') } - await showReviewRequest() response(true) } catch (error) { console.error('Failed to execute openPopupAndClick:', error) diff --git a/src/components/App.tsx b/src/components/App.tsx index fc068fb3..9ffe1415 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -11,6 +11,9 @@ import { getSelectionText } from '@/services/dom' import { SelectContextProvider } from '@/hooks/useSelectContext' import { PageActionContextProvider } from '@/hooks/pageAction/usePageActionContext' import { Ipc, TabCommand } from '@/services/ipc' +import { showReviewRequestToast } from '@/components/ReviewRequestToast' +import { Settings } from '@/services/settings' +import { Toaster } from 'sonner' export function App() { const [positionElm, setPositionElm] = useState(null) @@ -37,6 +40,20 @@ export function App() { } }, [isHover]) + useEffect(() => { + const handleShowReviewRequest = () => { + showReviewRequestToast(() => { + Settings.update('hasShownReviewRequest', () => true) + }) + return true + } + + Ipc.addListener(TabCommand.showReviewRequest, handleShowReviewRequest) + return () => { + Ipc.removeListener(TabCommand.showReviewRequest) + } + }, []) + return ( @@ -50,6 +67,7 @@ export function App() { + ) diff --git a/src/components/ReviewRequestToast.tsx b/src/components/ReviewRequestToast.tsx new file mode 100644 index 00000000..2a397b01 --- /dev/null +++ b/src/components/ReviewRequestToast.tsx @@ -0,0 +1,48 @@ +import { Star } from 'lucide-react' +import { toast } from 'sonner' +import { t } from '@/services/i18n' + +const REVIEW_URL = 'https://chromewebstore.google.com/detail/nlnhbibaommoelemmdfnkjkgoppkohje/reviews' + +export function showReviewRequestToast(onClose: () => void): void { + toast.custom( + (id) => ( +
+ +
+

+ {t('review_request_title')} +

+

+ {t('review_request_message')} +

+
+ + +
+
+
+ ), + { + duration: Infinity, + position: 'bottom-right', + } + ) +} \ No newline at end of file diff --git a/src/services/ipc.ts b/src/services/ipc.ts index ee42ef51..ea724e6e 100644 --- a/src/services/ipc.ts +++ b/src/services/ipc.ts @@ -35,6 +35,7 @@ export enum TabCommand { clickElement = 'clickElement', closeMenu = 'closeMenu', getTabId = 'getTabId', + showReviewRequest = 'showReviewRequest', // PageAction sendWindowSize = 'sendWindowSize', execPageAction = 'execPageAction', From cfed6943198b5c2802f05fbd130107f8c81272bc Mon Sep 17 00:00:00 2001 From: ujiro99 Date: Wed, 14 May 2025 22:14:33 +0900 Subject: [PATCH 06/10] Refactoring: UserSettings. --- src/components/option/ImportExport.tsx | 17 +++++++++----- src/services/defaultSettings.ts | 6 ++--- src/services/settings.ts | 31 ++++++++++++++------------ src/services/storage.ts | 5 +++++ src/types.ts | 24 +++++++++++++------- 5 files changed, 51 insertions(+), 32 deletions(-) diff --git a/src/components/option/ImportExport.tsx b/src/components/option/ImportExport.tsx index 8a431935..086d3ca6 100644 --- a/src/components/option/ImportExport.tsx +++ b/src/components/option/ImportExport.tsx @@ -1,6 +1,6 @@ import { useState, useRef } from 'react' import { Dialog } from './Dialog' -import type { SettingsType } from '@/types' +import type { UserSettings } from '@/types' import { Storage, STORAGE_KEY } from '@/services/storage' import { Settings, migrate } from '@/services/settings' @@ -24,7 +24,7 @@ function getTimestamp() { export function ImportExport() { const [resetDialog, setResetDialog] = useState(false) const [importDialog, setImportDialog] = useState(false) - const [importJson, setImportJson] = useState() + const [importJson, setImportJson] = useState() const inputFile = useRef(null) const handleReset = () => { @@ -39,7 +39,7 @@ export function ImportExport() { } const handleExport = async () => { - const data = await Storage.get(STORAGE_KEY.USER) + const data = await Storage.get(STORAGE_KEY.USER) data.commands = await Storage.getCommands() // for back compatibility @@ -84,9 +84,14 @@ export function ImportExport() { const handleImportClose = (ret: boolean) => { if (ret && importJson != null) { ; (async () => { - const preservedValues = await Settings.getPreservedValues() - const data = await migrate(importJson) - await Settings.set({ ...data, ...preservedValues }) + const { commandExecutionCount = 0, hasShownReviewRequest = false } = await Settings.get() + const data = await migrate({ + ...importJson, + commandExecutionCount, + hasShownReviewRequest, + stars: [] + }) + await Settings.set(data) location.reload() })() } diff --git a/src/services/defaultSettings.ts b/src/services/defaultSettings.ts index 23b21855..a84429e1 100644 --- a/src/services/defaultSettings.ts +++ b/src/services/defaultSettings.ts @@ -1,4 +1,4 @@ -import { SettingsType, Command } from '@/types' +import { UserSettings, Command } from '@/types' import { VERSION, OPEN_MODE, @@ -87,9 +87,7 @@ export default { }, ], stars: [], - commandExecutionCount: 0, - hasShownReviewRequest: false, -} as SettingsType +} as UserSettings export const PopupOption = { width: 600, diff --git a/src/services/settings.ts b/src/services/settings.ts index 512ce0be..8a76292e 100644 --- a/src/services/settings.ts +++ b/src/services/settings.ts @@ -11,7 +11,7 @@ import { SIDE, ALIGN, } from '@/const' -import type { SettingsType, Version, Command, Star } from '@/types' +import type { SettingsType, UserSettings, Version, Command, Star, UserStats } from '@/types' import { isBase64, isEmpty, @@ -57,6 +57,10 @@ export const Settings = { // Stars data.stars = await Storage.get(LOCAL_STORAGE_KEY.STARS) + // UserStats + const userStats = await Storage.get(STORAGE_KEY.USER_STATS) + data = { ...data, ...userStats } + data = await migrate(data) data.folders = data.folders.filter((folder) => !!folder.title) @@ -121,10 +125,18 @@ export const Settings = { data.commands = [] // Stars - await Storage.set(LOCAL_STORAGE_KEY.STARS, data.stars) - data.stars = [] + await Storage.set(LOCAL_STORAGE_KEY.STARS, data.stars) + + // UserStats + const userStats: UserStats = { + commandExecutionCount: data.commandExecutionCount, + hasShownReviewRequest: data.hasShownReviewRequest, + } + await Storage.set(STORAGE_KEY.USER_STATS, userStats) - await Storage.set(STORAGE_KEY.USER, data) + // Remove UserStats and stars from data + const { commandExecutionCount, hasShownReviewRequest, stars, ...restData } = data + await Storage.set(STORAGE_KEY.USER, restData) await Storage.set(LOCAL_STORAGE_KEY.CACHES, caches) return true }, @@ -154,8 +166,7 @@ export const Settings = { }, reset: async () => { - const preservedValues = await Settings.getPreservedValues() - await Storage.set(STORAGE_KEY.USER, { ...DefaultSettings, ...preservedValues }) + await Storage.set(STORAGE_KEY.USER, DefaultSettings) await Storage.setCommands(DefaultCommands) }, @@ -172,14 +183,6 @@ export const Settings = { return Storage.get(LOCAL_STORAGE_KEY.CACHES) }, - getPreservedValues: async (): Promise> => { - const currentSettings = await Settings.get() - return { - commandExecutionCount: currentSettings.commandExecutionCount, - hasShownReviewRequest: currentSettings.hasShownReviewRequest, - } - }, - getUrls: (settings: SettingsType): string[] => { const iconUrls = settings.commands.map((c) => c.iconUrl) const folderIconUrls = settings.folders.map((f) => f.iconUrl) diff --git a/src/services/storage.ts b/src/services/storage.ts index 3b490061..ff501ed6 100644 --- a/src/services/storage.ts +++ b/src/services/storage.ts @@ -4,6 +4,7 @@ import { Command, CaptureDataStorage } from '@/types' export enum STORAGE_KEY { USER = 0, COMMAND_COUNT = 2, + USER_STATS = 3, } export enum LOCAL_STORAGE_KEY { @@ -33,6 +34,10 @@ const DEFAULT_COUNT = -1 const DEFAULTS = { [STORAGE_KEY.USER]: DefaultSettings, [STORAGE_KEY.COMMAND_COUNT]: DEFAULT_COUNT, + [STORAGE_KEY.USER_STATS]: { + commandExecutionCount: 0, + hasShownReviewRequest: false, + }, [LOCAL_STORAGE_KEY.CACHES]: { images: {}, }, diff --git a/src/types.ts b/src/types.ts index 60a5f2a1..8df76df9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -118,10 +118,6 @@ export type StartupMethod = { leftClickHoldParam?: number } -export type Star = { - id: string -} - export type PopupPlacement = { side: SIDE align: ALIGN @@ -129,7 +125,20 @@ export type PopupPlacement = { alignOffset: number } -export type SettingsType = { +export type Star = { + id: string +} + +type UserStars = { + stars: Array +} + +export type UserStats = { + commandExecutionCount: number + hasShownReviewRequest: boolean +} + +export type UserSettings = { settingVersion: Version startupMethod: StartupMethod popupPlacement: PopupPlacement @@ -139,11 +148,10 @@ export type SettingsType = { pageRules: Array style: STYLE userStyles: Array - stars: Array - commandExecutionCount?: number - hasShownReviewRequest?: boolean } +export type SettingsType = UserSettings & UserStats & UserStars + export type SessionData = { session_id: string timestamp: number From ca75cd213d7ad0f2800544858baf7a2bf50cbba8 Mon Sep 17 00:00:00 2001 From: ujiro99 Date: Thu, 15 May 2025 15:25:43 +0900 Subject: [PATCH 07/10] Update: Change the toast ui library. --- package.json | 3 +- src/background_script.ts | 27 ++-- src/components/App.tsx | 16 ++- src/components/ReviewRequestToast.tsx | 56 ++------ src/components/ui/sonner.tsx | 31 ---- src/components/ui/toast.tsx | 129 +++++++++++++++++ src/components/ui/toaster.tsx | 35 +++++ src/hooks/useToast.tsx | 194 ++++++++++++++++++++++++++ src/services/settings.ts | 3 +- yarn.lock | 93 ++++++++++-- 10 files changed, 482 insertions(+), 105 deletions(-) delete mode 100644 src/components/ui/sonner.tsx create mode 100644 src/components/ui/toast.tsx create mode 100644 src/components/ui/toaster.tsx create mode 100644 src/hooks/useToast.tsx diff --git a/package.json b/package.json index 3591bdbe..958efdd7 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@radix-ui/react-progress": "^1.1.2", "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-switch": "^1.1.3", + "@radix-ui/react-toast": "^1.2.13", "@radix-ui/react-toggle": "^1.1.6", "@radix-ui/react-toggle-group": "^1.1.7", "@testing-library/user-event": "^14.6.0", @@ -36,14 +37,12 @@ "get-xpath": "^3.3.0", "lottie-web": "^5.12.2", "lucide-react": "^0.483.0", - "next-themes": "^0.4.6", "platform": "^1.3.6", "react": "^18.3.1", "react-dom": "^18.3.1", "react-hook-form": "^7.54.2", "react-textarea-autosize": "^8.5.3", "react-transition-group": "^4.4.5", - "sonner": "^2.0.3", "tailwind-merge": "^2.5.4", "tailwindcss-animate": "^1.0.7", "zod": "^3.24.1" diff --git a/src/background_script.ts b/src/background_script.ts index e1db8cdd..92ce2cf3 100644 --- a/src/background_script.ts +++ b/src/background_script.ts @@ -41,6 +41,7 @@ const incrementCommandExecutionCount = async (): Promise => { await Settings.update( CONSTANTS.SETTING_KEY.COMMAND_EXECUTION_COUNT, () => count + 1, + true, ) // 閾値を超えたらレビュー依頼を表示 @@ -164,7 +165,7 @@ const commandFuncs = { await Settings.set({ ...settings, pageRules, - }) + }, true) chrome.tabs.create({ url: `${CONSTANTS.OPTION_PAGE}#pageRules`, }) @@ -184,24 +185,24 @@ const commandFuncs = { const cmd = isSearch ? { + id: params.id, + title: params.title, + searchUrl: params.searchUrl, + iconUrl: params.iconUrl, + openMode: params.openMode, + openModeSecondary: params.openModeSecondary, + spaceEncoding: params.spaceEncoding, + popupOption: PopupOption, + } + : isPageAction + ? { id: params.id, title: params.title, - searchUrl: params.searchUrl, iconUrl: params.iconUrl, openMode: params.openMode, - openModeSecondary: params.openModeSecondary, - spaceEncoding: params.spaceEncoding, + pageActionOption: params.pageActionOption, popupOption: PopupOption, } - : isPageAction - ? { - id: params.id, - title: params.title, - iconUrl: params.iconUrl, - openMode: params.openMode, - pageActionOption: params.pageActionOption, - popupOption: PopupOption, - } : null if (!cmd) { diff --git a/src/components/App.tsx b/src/components/App.tsx index 9ffe1415..27a5b6ce 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -11,15 +11,16 @@ import { getSelectionText } from '@/services/dom' import { SelectContextProvider } from '@/hooks/useSelectContext' import { PageActionContextProvider } from '@/hooks/pageAction/usePageActionContext' import { Ipc, TabCommand } from '@/services/ipc' +import { Toaster } from "@/components/ui/toaster" +import { useToast } from "@/hooks/useToast" import { showReviewRequestToast } from '@/components/ReviewRequestToast' -import { Settings } from '@/services/settings' -import { Toaster } from 'sonner' export function App() { const [positionElm, setPositionElm] = useState(null) const [target, setTarget] = useState(null) const [isHover, setIsHover] = useState(false) const [selectionText, setSelectionText] = useState('') + const { toast } = useToast() useEffect(() => { Ipc.addListener(TabCommand.connect, () => false) @@ -41,10 +42,13 @@ export function App() { }, [isHover]) useEffect(() => { - const handleShowReviewRequest = () => { - showReviewRequestToast(() => { - Settings.update('hasShownReviewRequest', () => true) - }) + const handleShowReviewRequest = (_param: any, _sender: any, response: any) => { + showReviewRequestToast(toast) + + //showReviewRequestToast(() => { + // Settings.update('hasShownReviewRequest', () => true) + //}) + response(true) return true } diff --git a/src/components/ReviewRequestToast.tsx b/src/components/ReviewRequestToast.tsx index 2a397b01..bdfe6dad 100644 --- a/src/components/ReviewRequestToast.tsx +++ b/src/components/ReviewRequestToast.tsx @@ -1,48 +1,20 @@ import { Star } from 'lucide-react' -import { toast } from 'sonner' import { t } from '@/services/i18n' +import { ToastAction } from "@/components/ui/toast" + const REVIEW_URL = 'https://chromewebstore.google.com/detail/nlnhbibaommoelemmdfnkjkgoppkohje/reviews' -export function showReviewRequestToast(onClose: () => void): void { - toast.custom( - (id) => ( -
- -
-

- {t('review_request_title')} -

-

- {t('review_request_message')} -

-
- - -
-
-
- ), - { - duration: Infinity, - position: 'bottom-right', - } - ) +export function showReviewRequestToast(toast: any): void { + toast({ + title: t("review_request_title"), + description: t("review_request_message"), + action: { + window.open(REVIEW_URL, '_blank') + }} + >{t("review_request_button")}, + duration: 100 * 1000, + }) + } \ No newline at end of file diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx deleted file mode 100644 index a8d15df3..00000000 --- a/src/components/ui/sonner.tsx +++ /dev/null @@ -1,31 +0,0 @@ -"use client" - -import { useTheme } from "next-themes" -import { Toaster as Sonner } from "sonner" - -type ToasterProps = React.ComponentProps - -const Toaster = ({ ...props }: ToasterProps) => { - const { theme = "system" } = useTheme() - - return ( - - ) -} - -export { Toaster } \ No newline at end of file diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx new file mode 100644 index 00000000..5ea0563b --- /dev/null +++ b/src/components/ui/toast.tsx @@ -0,0 +1,129 @@ +"use client" + +import * as React from "react" +import * as ToastPrimitives from "@radix-ui/react-toast" +import { cva, type VariantProps } from "class-variance-authority" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const ToastProvider = ToastPrimitives.Provider + +const ToastViewport = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastViewport.displayName = ToastPrimitives.Viewport.displayName + +const toastVariants = cva( + "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", + { + variants: { + variant: { + default: "border bg-background text-foreground", + destructive: + "destructive group border-destructive bg-destructive text-destructive-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Toast = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, ...props }, ref) => { + return ( + + ) +}) +Toast.displayName = ToastPrimitives.Root.displayName + +const ToastAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastAction.displayName = ToastPrimitives.Action.displayName + +const ToastClose = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +ToastClose.displayName = ToastPrimitives.Close.displayName + +const ToastTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastTitle.displayName = ToastPrimitives.Title.displayName + +const ToastDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastDescription.displayName = ToastPrimitives.Description.displayName + +type ToastProps = React.ComponentPropsWithoutRef + +type ToastActionElement = React.ReactElement + +export { + type ToastProps, + type ToastActionElement, + ToastProvider, + ToastViewport, + Toast, + ToastTitle, + ToastDescription, + ToastClose, + ToastAction, +} \ No newline at end of file diff --git a/src/components/ui/toaster.tsx b/src/components/ui/toaster.tsx new file mode 100644 index 00000000..af810bab --- /dev/null +++ b/src/components/ui/toaster.tsx @@ -0,0 +1,35 @@ +"use client" + +import { useToast } from "@/hooks/useToast" +import { + Toast, + ToastClose, + ToastDescription, + ToastProvider, + ToastTitle, + ToastViewport, +} from "@/components/ui/toast" + +export function Toaster() { + const { toasts } = useToast() + + return ( + + {toasts.map(function ({ id, title, description, action, ...props }) { + return ( + +
+ {title && {title}} + {description && ( + {description} + )} +
+ {action} + +
+ ) + })} + +
+ ) +} diff --git a/src/hooks/useToast.tsx b/src/hooks/useToast.tsx new file mode 100644 index 00000000..4e4408a7 --- /dev/null +++ b/src/hooks/useToast.tsx @@ -0,0 +1,194 @@ +"use client" + +// Inspired by react-hot-toast library +import * as React from "react" + +import type { + ToastActionElement, + ToastProps, +} from "@/components/ui/toast" + +const TOAST_LIMIT = 1 +const TOAST_REMOVE_DELAY = 1000000 + +type ToasterToast = ToastProps & { + id: string + title?: React.ReactNode + description?: React.ReactNode + action?: ToastActionElement +} + +const actionTypes = { + ADD_TOAST: "ADD_TOAST", + UPDATE_TOAST: "UPDATE_TOAST", + DISMISS_TOAST: "DISMISS_TOAST", + REMOVE_TOAST: "REMOVE_TOAST", +} as const + +let count = 0 + +function genId() { + count = (count + 1) % Number.MAX_SAFE_INTEGER + return count.toString() +} + +type ActionType = typeof actionTypes + +type Action = + | { + type: ActionType["ADD_TOAST"] + toast: ToasterToast + } + | { + type: ActionType["UPDATE_TOAST"] + toast: Partial + } + | { + type: ActionType["DISMISS_TOAST"] + toastId?: ToasterToast["id"] + } + | { + type: ActionType["REMOVE_TOAST"] + toastId?: ToasterToast["id"] + } + +interface State { + toasts: ToasterToast[] +} + +const toastTimeouts = new Map>() + +const addToRemoveQueue = (toastId: string) => { + if (toastTimeouts.has(toastId)) { + return + } + + const timeout = setTimeout(() => { + toastTimeouts.delete(toastId) + dispatch({ + type: "REMOVE_TOAST", + toastId: toastId, + }) + }, TOAST_REMOVE_DELAY) + + toastTimeouts.set(toastId, timeout) +} + +export const reducer = (state: State, action: Action): State => { + switch (action.type) { + case "ADD_TOAST": + return { + ...state, + toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), + } + + case "UPDATE_TOAST": + return { + ...state, + toasts: state.toasts.map((t) => + t.id === action.toast.id ? { ...t, ...action.toast } : t + ), + } + + case "DISMISS_TOAST": { + const { toastId } = action + + // ! Side effects ! - This could be extracted into a dismissToast() action, + // but I'll keep it here for simplicity + if (toastId) { + addToRemoveQueue(toastId) + } else { + state.toasts.forEach((toast) => { + addToRemoveQueue(toast.id) + }) + } + + return { + ...state, + toasts: state.toasts.map((t) => + t.id === toastId || toastId === undefined + ? { + ...t, + open: false, + } + : t + ), + } + } + case "REMOVE_TOAST": + if (action.toastId === undefined) { + return { + ...state, + toasts: [], + } + } + return { + ...state, + toasts: state.toasts.filter((t) => t.id !== action.toastId), + } + } +} + +const listeners: Array<(state: State) => void> = [] + +let memoryState: State = { toasts: [] } + +function dispatch(action: Action) { + memoryState = reducer(memoryState, action) + listeners.forEach((listener) => { + listener(memoryState) + }) +} + +type Toast = Omit + +function toast({ ...props }: Toast) { + const id = genId() + + const update = (props: ToasterToast) => + dispatch({ + type: "UPDATE_TOAST", + toast: { ...props, id }, + }) + const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) + + dispatch({ + type: "ADD_TOAST", + toast: { + ...props, + id, + open: true, + onOpenChange: (open) => { + if (!open) dismiss() + }, + }, + }) + + return { + id: id, + dismiss, + update, + } +} + +function useToast() { + const [state, setState] = React.useState(memoryState) + + React.useEffect(() => { + listeners.push(setState) + return () => { + const index = listeners.indexOf(setState) + if (index > -1) { + listeners.splice(index, 1) + } + } + }, [state]) + + return { + ...state, + toast, + dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), + } +} + +export { useToast, toast } diff --git a/src/services/settings.ts b/src/services/settings.ts index 8a76292e..870e018d 100644 --- a/src/services/settings.ts +++ b/src/services/settings.ts @@ -144,13 +144,14 @@ export const Settings = { update: async ( key: T, updater: (value: SettingsType[T]) => SettingsType[T], + serviceWorker = false, ): Promise => { const settings = await Settings.get() const updatedSettings = { ...settings, [key]: updater(settings[key]), } - return Settings.set(updatedSettings) + return Settings.set(updatedSettings, serviceWorker) }, addCommands: async (commands: Command[]): Promise => { diff --git a/yarn.lock b/yarn.lock index 31d09c48..e62e6fa1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -780,6 +780,16 @@ "@radix-ui/react-primitive" "2.1.0" "@radix-ui/react-slot" "1.2.0" +"@radix-ui/react-collection@1.1.6": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.6.tgz#fecf74475e4660ee99c7eb1ebfa5ccfb1a219fe4" + integrity sha512-PbhRFK4lIEw9ADonj48tiYWzkllz81TM7KVYyyMMw2cwHO7D5h4XKEblL8NlaRisTK3QTe6tBEhDccFUryxHBQ== + dependencies: + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-primitive" "2.1.2" + "@radix-ui/react-slot" "1.2.2" + "@radix-ui/react-compose-refs@1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz#6f766faa975f8738269ebb8a23bad4f5a8d2faec" @@ -852,6 +862,17 @@ "@radix-ui/react-use-callback-ref" "1.1.0" "@radix-ui/react-use-escape-keydown" "1.1.0" +"@radix-ui/react-dismissable-layer@1.1.9": + version "1.1.9" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.9.tgz#46e025ba6e6f403677e22fbb7d99b63cf7b32bca" + integrity sha512-way197PiTvNp+WBP7svMJasHl+vibhWGQDb6Mgf5mhEWJkgb85z7Lfl9TUdkqpWsf8GRNmoopx9ZxCyDzmgRMQ== + dependencies: + "@radix-ui/primitive" "1.1.2" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-primitive" "2.1.2" + "@radix-ui/react-use-callback-ref" "1.1.1" + "@radix-ui/react-use-escape-keydown" "1.1.1" + "@radix-ui/react-focus-guards@1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz#8635edd346304f8b42cae86b05912b61aef27afe" @@ -1005,6 +1026,14 @@ "@radix-ui/react-primitive" "2.0.2" "@radix-ui/react-use-layout-effect" "1.1.0" +"@radix-ui/react-portal@1.1.8": + version "1.1.8" + resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.8.tgz#0181e85bc0d8c67229dd8cf198204f5f4cc7c09c" + integrity sha512-hQsTUIn7p7fxCPvao/q6wpbxmCwgLrlz+nOrJgC+RwfZqWY/WN+UMqkXzrtKbPrF82P43eCTl3ekeKuyAQbFeg== + dependencies: + "@radix-ui/react-primitive" "2.1.2" + "@radix-ui/react-use-layout-effect" "1.1.1" + "@radix-ui/react-presence@1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.2.tgz#bb764ed8a9118b7ec4512da5ece306ded8703cdc" @@ -1013,6 +1042,14 @@ "@radix-ui/react-compose-refs" "1.1.1" "@radix-ui/react-use-layout-effect" "1.1.0" +"@radix-ui/react-presence@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.4.tgz#253ac0ad4946c5b4a9c66878335f5cf07c967ced" + integrity sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA== + dependencies: + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-use-layout-effect" "1.1.1" + "@radix-ui/react-primitive@2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz#6d9efc550f7520135366f333d1e820cf225fad9e" @@ -1034,6 +1071,13 @@ dependencies: "@radix-ui/react-slot" "1.2.0" +"@radix-ui/react-primitive@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.1.2.tgz#03f64f957719c761d22c2f92cc43ffb64bd42cc8" + integrity sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw== + dependencies: + "@radix-ui/react-slot" "1.2.2" + "@radix-ui/react-progress@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@radix-ui/react-progress/-/react-progress-1.1.2.tgz#3584c346d47f2a6f86076ce5af56ab00c66ded2b" @@ -1120,6 +1164,13 @@ dependencies: "@radix-ui/react-compose-refs" "1.1.2" +"@radix-ui/react-slot@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.2.2.tgz#18e6533e778a2051edc2ad0773da8e22f03f626a" + integrity sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ== + dependencies: + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-switch@^1.1.3": version "1.1.3" resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.1.3.tgz#cb6386909d1d3f65a2b81a3b15da8c91d18f49b0" @@ -1133,6 +1184,24 @@ "@radix-ui/react-use-previous" "1.1.0" "@radix-ui/react-use-size" "1.1.0" +"@radix-ui/react-toast@^1.2.13": + version "1.2.13" + resolved "https://registry.yarnpkg.com/@radix-ui/react-toast/-/react-toast-1.2.13.tgz#e2b27456b52d1b1629becb0299912fd842dc5afe" + integrity sha512-e/e43mQAwgYs8BY4y9l99xTK6ig1bK2uXsFLOMn9IZ16lAgulSTsotcPHVT2ZlSb/ye6Sllq7IgyDB8dGhpeXQ== + dependencies: + "@radix-ui/primitive" "1.1.2" + "@radix-ui/react-collection" "1.1.6" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-dismissable-layer" "1.1.9" + "@radix-ui/react-portal" "1.1.8" + "@radix-ui/react-presence" "1.1.4" + "@radix-ui/react-primitive" "2.1.2" + "@radix-ui/react-use-callback-ref" "1.1.1" + "@radix-ui/react-use-controllable-state" "1.2.2" + "@radix-ui/react-use-layout-effect" "1.1.1" + "@radix-ui/react-visually-hidden" "1.2.2" + "@radix-ui/react-toggle-group@^1.1.7": version "1.1.7" resolved "https://registry.yarnpkg.com/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.7.tgz#0eaca9e4f8fbf2536f01e33a6211eac4d6cfb83e" @@ -1194,6 +1263,13 @@ dependencies: "@radix-ui/react-use-callback-ref" "1.1.0" +"@radix-ui/react-use-escape-keydown@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz#b3fed9bbea366a118f40427ac40500aa1423cc29" + integrity sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g== + dependencies: + "@radix-ui/react-use-callback-ref" "1.1.1" + "@radix-ui/react-use-layout-effect@1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz#3c2c8ce04827b26a39e442ff4888d9212268bd27" @@ -1230,6 +1306,13 @@ dependencies: "@radix-ui/react-primitive" "2.0.2" +"@radix-ui/react-visually-hidden@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.2.tgz#aa6d0f95b0cd50f08b02393d25132f52ca7861dc" + integrity sha512-ORCmRUbNiZIv6uV5mhFrhsIKw4UX/N3syZtyqvry61tbGm4JlgQuSn0hk5TwCARsCjkcnuRkSdCE3xfb+ADHew== + dependencies: + "@radix-ui/react-primitive" "2.1.2" + "@radix-ui/rect@1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.1.0.tgz#f817d1d3265ac5415dadc67edab30ae196696438" @@ -3535,11 +3618,6 @@ ndarray@^1.0.19: iota-array "^1.0.0" is-buffer "^1.0.2" -next-themes@^0.4.6: - version "0.4.6" - resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.4.6.tgz#8d7e92d03b8fea6582892a50a928c9b23502e8b6" - integrity sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA== - node-releases@^2.0.19: version "2.0.19" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" @@ -4373,11 +4451,6 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -sonner@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/sonner/-/sonner-2.0.3.tgz#de7cdbc4b6a25ac3f0a9e0aed3748e0b3d6e092e" - integrity sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA== - source-map-js@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" From 47c00a8010e751decb66d2223420e2e17ed41b3f Mon Sep 17 00:00:00 2001 From: ujiro99 Date: Thu, 15 May 2025 22:30:30 +0900 Subject: [PATCH 08/10] Update: Styling the toast ui for requesting review. --- pages/src/components/ui/toast.tsx | 129 +++++++++++++++++ pages/src/components/ui/toaster.tsx | 35 +++++ pages/src/hooks/use-toast.ts | 194 ++++++++++++++++++++++++++ public/_locales/en/messages.json | 2 +- public/_locales/ja/messages.json | 10 +- src/background_script.ts | 13 +- src/components/App.tsx | 9 +- src/components/ReviewRequestToast.tsx | 45 ++++-- src/components/ui/toaster.tsx | 2 +- 9 files changed, 408 insertions(+), 31 deletions(-) create mode 100644 pages/src/components/ui/toast.tsx create mode 100644 pages/src/components/ui/toaster.tsx create mode 100644 pages/src/hooks/use-toast.ts diff --git a/pages/src/components/ui/toast.tsx b/pages/src/components/ui/toast.tsx new file mode 100644 index 00000000..40ac9ddb --- /dev/null +++ b/pages/src/components/ui/toast.tsx @@ -0,0 +1,129 @@ +"use client" + +import * as React from "react" +import * as ToastPrimitives from "@radix-ui/react-toast" +import { cva, type VariantProps } from "class-variance-authority" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const ToastProvider = ToastPrimitives.Provider + +const ToastViewport = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastViewport.displayName = ToastPrimitives.Viewport.displayName + +const toastVariants = cva( + "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", + { + variants: { + variant: { + default: "border bg-background text-foreground", + destructive: + "destructive group border-destructive bg-destructive text-destructive-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Toast = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, ...props }, ref) => { + return ( + + ) +}) +Toast.displayName = ToastPrimitives.Root.displayName + +const ToastAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastAction.displayName = ToastPrimitives.Action.displayName + +const ToastClose = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +ToastClose.displayName = ToastPrimitives.Close.displayName + +const ToastTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastTitle.displayName = ToastPrimitives.Title.displayName + +const ToastDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastDescription.displayName = ToastPrimitives.Description.displayName + +type ToastProps = React.ComponentPropsWithoutRef + +type ToastActionElement = React.ReactElement + +export { + type ToastProps, + type ToastActionElement, + ToastProvider, + ToastViewport, + Toast, + ToastTitle, + ToastDescription, + ToastClose, + ToastAction, +} diff --git a/pages/src/components/ui/toaster.tsx b/pages/src/components/ui/toaster.tsx new file mode 100644 index 00000000..171beb46 --- /dev/null +++ b/pages/src/components/ui/toaster.tsx @@ -0,0 +1,35 @@ +"use client" + +import { useToast } from "@/hooks/use-toast" +import { + Toast, + ToastClose, + ToastDescription, + ToastProvider, + ToastTitle, + ToastViewport, +} from "@/components/ui/toast" + +export function Toaster() { + const { toasts } = useToast() + + return ( + + {toasts.map(function ({ id, title, description, action, ...props }) { + return ( + +
+ {title && {title}} + {description && ( + {description} + )} +
+ {action} + +
+ ) + })} + +
+ ) +} diff --git a/pages/src/hooks/use-toast.ts b/pages/src/hooks/use-toast.ts new file mode 100644 index 00000000..02e111d8 --- /dev/null +++ b/pages/src/hooks/use-toast.ts @@ -0,0 +1,194 @@ +"use client" + +// Inspired by react-hot-toast library +import * as React from "react" + +import type { + ToastActionElement, + ToastProps, +} from "@/components/ui/toast" + +const TOAST_LIMIT = 1 +const TOAST_REMOVE_DELAY = 1000000 + +type ToasterToast = ToastProps & { + id: string + title?: React.ReactNode + description?: React.ReactNode + action?: ToastActionElement +} + +const actionTypes = { + ADD_TOAST: "ADD_TOAST", + UPDATE_TOAST: "UPDATE_TOAST", + DISMISS_TOAST: "DISMISS_TOAST", + REMOVE_TOAST: "REMOVE_TOAST", +} as const + +let count = 0 + +function genId() { + count = (count + 1) % Number.MAX_SAFE_INTEGER + return count.toString() +} + +type ActionType = typeof actionTypes + +type Action = + | { + type: ActionType["ADD_TOAST"] + toast: ToasterToast + } + | { + type: ActionType["UPDATE_TOAST"] + toast: Partial + } + | { + type: ActionType["DISMISS_TOAST"] + toastId?: ToasterToast["id"] + } + | { + type: ActionType["REMOVE_TOAST"] + toastId?: ToasterToast["id"] + } + +interface State { + toasts: ToasterToast[] +} + +const toastTimeouts = new Map>() + +const addToRemoveQueue = (toastId: string) => { + if (toastTimeouts.has(toastId)) { + return + } + + const timeout = setTimeout(() => { + toastTimeouts.delete(toastId) + dispatch({ + type: "REMOVE_TOAST", + toastId: toastId, + }) + }, TOAST_REMOVE_DELAY) + + toastTimeouts.set(toastId, timeout) +} + +export const reducer = (state: State, action: Action): State => { + switch (action.type) { + case "ADD_TOAST": + return { + ...state, + toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), + } + + case "UPDATE_TOAST": + return { + ...state, + toasts: state.toasts.map((t) => + t.id === action.toast.id ? { ...t, ...action.toast } : t + ), + } + + case "DISMISS_TOAST": { + const { toastId } = action + + // ! Side effects ! - This could be extracted into a dismissToast() action, + // but I'll keep it here for simplicity + if (toastId) { + addToRemoveQueue(toastId) + } else { + state.toasts.forEach((toast) => { + addToRemoveQueue(toast.id) + }) + } + + return { + ...state, + toasts: state.toasts.map((t) => + t.id === toastId || toastId === undefined + ? { + ...t, + open: false, + } + : t + ), + } + } + case "REMOVE_TOAST": + if (action.toastId === undefined) { + return { + ...state, + toasts: [], + } + } + return { + ...state, + toasts: state.toasts.filter((t) => t.id !== action.toastId), + } + } +} + +const listeners: Array<(state: State) => void> = [] + +let memoryState: State = { toasts: [] } + +function dispatch(action: Action) { + memoryState = reducer(memoryState, action) + listeners.forEach((listener) => { + listener(memoryState) + }) +} + +type Toast = Omit + +function toast({ ...props }: Toast) { + const id = genId() + + const update = (props: ToasterToast) => + dispatch({ + type: "UPDATE_TOAST", + toast: { ...props, id }, + }) + const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) + + dispatch({ + type: "ADD_TOAST", + toast: { + ...props, + id, + open: true, + onOpenChange: (open) => { + if (!open) dismiss() + }, + }, + }) + + return { + id: id, + dismiss, + update, + } +} + +function useToast() { + const [state, setState] = React.useState(memoryState) + + React.useEffect(() => { + listeners.push(setState) + return () => { + const index = listeners.indexOf(setState) + if (index > -1) { + listeners.splice(index, 1) + } + } + }, [state]) + + return { + ...state, + toast, + dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), + } +} + +export { useToast, toast } diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json index 1713afc6..b871e4d3 100644 --- a/public/_locales/en/messages.json +++ b/public/_locales/en/messages.json @@ -734,4 +734,4 @@ "review_request_close": { "message": "Not Now" } -} +} \ No newline at end of file diff --git a/public/_locales/ja/messages.json b/public/_locales/ja/messages.json index 44091ddb..3dc35d93 100644 --- a/public/_locales/ja/messages.json +++ b/public/_locales/ja/messages.json @@ -720,15 +720,15 @@ "message": "Selection Command Hubから、Page Actionコマンドの共有と取得ができます。 " }, "review_request_title": { - "message": "Selection Commandをお楽しみいただいていますか?" + "message": "Selection Command はいかがですか?" }, "review_request_message": { - "message": "この拡張機能がお役に立っていると感じていただけましたら、Chrome Web Storeでのレビューをお願いいたします。皆様のフィードバックが改善の糧となります!" + "message": "この拡張機能を気に入っていただけたら、Chrome Web Storeでのレビューをお願いします。皆様のフィードバックが改善の糧となります!" }, "review_request_button": { - "message": "レビューを書く" + "message": "レビューする" }, - "review_request_close": { + "review_request_later": { "message": "後で" } -} +} \ No newline at end of file diff --git a/src/background_script.ts b/src/background_script.ts index 92ce2cf3..94985751 100644 --- a/src/background_script.ts +++ b/src/background_script.ts @@ -21,11 +21,11 @@ import { Storage, SESSION_STORAGE_KEY } from './services/storage' const CONSTANTS = { OPTION_PAGE: 'src/options_page.html', REVIEW_THRESHOLD: 100, + REVIEW_INTERVAL: 50, SETTING_KEY: { COMMAND_EXECUTION_COUNT: 'commandExecutionCount', HAS_SHOWN_REVIEW_REQUEST: 'hasShownReviewRequest', }, - COMMAND_COUNT_THRESHOLD: 10, } as const BgData.init() @@ -34,18 +34,19 @@ BgData.init() const incrementCommandExecutionCount = async (): Promise => { try { const settings = await Settings.get() - const count = settings.commandExecutionCount ?? 0 + let count = settings.commandExecutionCount ?? 0 const hasShown = settings.hasShownReviewRequest ?? false - // コマンド実行回数をカウントアップ + // Increment command execution count + count++ await Settings.update( CONSTANTS.SETTING_KEY.COMMAND_EXECUTION_COUNT, - () => count + 1, + () => count, true, ) - // 閾値を超えたらレビュー依頼を表示 - if (count >= CONSTANTS.REVIEW_THRESHOLD && !hasShown) { + // Show review request when threshold is exceeded + if ((count === CONSTANTS.REVIEW_THRESHOLD || (count > CONSTANTS.REVIEW_THRESHOLD && (count - CONSTANTS.REVIEW_THRESHOLD) % CONSTANTS.REVIEW_INTERVAL === 0)) && !hasShown) { const tabs = await chrome.tabs.query({ active: true }) if (tabs[0]?.id) { await Ipc.sendTab(tabs[0].id, TabCommand.showReviewRequest) diff --git a/src/components/App.tsx b/src/components/App.tsx index 27a5b6ce..07862d36 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -14,6 +14,7 @@ import { Ipc, TabCommand } from '@/services/ipc' import { Toaster } from "@/components/ui/toaster" import { useToast } from "@/hooks/useToast" import { showReviewRequestToast } from '@/components/ReviewRequestToast' +import { Settings } from '@/services/settings' export function App() { const [positionElm, setPositionElm] = useState(null) @@ -43,11 +44,9 @@ export function App() { useEffect(() => { const handleShowReviewRequest = (_param: any, _sender: any, response: any) => { - showReviewRequestToast(toast) - - //showReviewRequestToast(() => { - // Settings.update('hasShownReviewRequest', () => true) - //}) + showReviewRequestToast(toast, () => { + Settings.update('hasShownReviewRequest', () => true) + }) response(true) return true } diff --git a/src/components/ReviewRequestToast.tsx b/src/components/ReviewRequestToast.tsx index bdfe6dad..b2cd6de3 100644 --- a/src/components/ReviewRequestToast.tsx +++ b/src/components/ReviewRequestToast.tsx @@ -1,20 +1,39 @@ -import { Star } from 'lucide-react' import { t } from '@/services/i18n' import { ToastAction } from "@/components/ui/toast" - const REVIEW_URL = 'https://chromewebstore.google.com/detail/nlnhbibaommoelemmdfnkjkgoppkohje/reviews' +const ICON_URL = chrome.runtime.getURL('icon128.png') -export function showReviewRequestToast(toast: any): void { - toast({ - title: t("review_request_title"), - description: t("review_request_message"), - action: { - window.open(REVIEW_URL, '_blank') - }} - >{t("review_request_button")}, - duration: 100 * 1000, +export function showReviewRequestToast(toast: any, onAccept: () => void): void { + const tst = toast({ + title: ( +

+ + + {t("review_request_title")} + +

+ ), + description: + {t("review_request_message")} + , + className: 'flex flex-col text-gray-800', + action:
+ { + // Close the toast + tst.dismiss() + }} + >{t("review_request_later")} + { + window.open(REVIEW_URL, '_blank') + onAccept() + }} + >🎉 {t("review_request_button")} +
, + duration: 60 * 1000, }) - } \ No newline at end of file diff --git a/src/components/ui/toaster.tsx b/src/components/ui/toaster.tsx index af810bab..b7ffff36 100644 --- a/src/components/ui/toaster.tsx +++ b/src/components/ui/toaster.tsx @@ -18,7 +18,7 @@ export function Toaster() { {toasts.map(function ({ id, title, description, action, ...props }) { return ( -
+
{title && {title}} {description && ( {description} From 2c151cee7d4a347a670fc48d9937ea7c6bf15e74 Mon Sep 17 00:00:00 2001 From: ujiro99 Date: Thu, 15 May 2025 23:24:03 +0900 Subject: [PATCH 09/10] Update: Remove unused files. --- pages/src/components/ui/toast.tsx | 129 ------------------ pages/src/components/ui/toaster.tsx | 35 ----- pages/src/hooks/use-toast.ts | 194 ---------------------------- 3 files changed, 358 deletions(-) delete mode 100644 pages/src/components/ui/toast.tsx delete mode 100644 pages/src/components/ui/toaster.tsx delete mode 100644 pages/src/hooks/use-toast.ts diff --git a/pages/src/components/ui/toast.tsx b/pages/src/components/ui/toast.tsx deleted file mode 100644 index 40ac9ddb..00000000 --- a/pages/src/components/ui/toast.tsx +++ /dev/null @@ -1,129 +0,0 @@ -"use client" - -import * as React from "react" -import * as ToastPrimitives from "@radix-ui/react-toast" -import { cva, type VariantProps } from "class-variance-authority" -import { X } from "lucide-react" - -import { cn } from "@/lib/utils" - -const ToastProvider = ToastPrimitives.Provider - -const ToastViewport = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -ToastViewport.displayName = ToastPrimitives.Viewport.displayName - -const toastVariants = cva( - "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", - { - variants: { - variant: { - default: "border bg-background text-foreground", - destructive: - "destructive group border-destructive bg-destructive text-destructive-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -const Toast = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & - VariantProps ->(({ className, variant, ...props }, ref) => { - return ( - - ) -}) -Toast.displayName = ToastPrimitives.Root.displayName - -const ToastAction = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -ToastAction.displayName = ToastPrimitives.Action.displayName - -const ToastClose = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - -)) -ToastClose.displayName = ToastPrimitives.Close.displayName - -const ToastTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -ToastTitle.displayName = ToastPrimitives.Title.displayName - -const ToastDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -ToastDescription.displayName = ToastPrimitives.Description.displayName - -type ToastProps = React.ComponentPropsWithoutRef - -type ToastActionElement = React.ReactElement - -export { - type ToastProps, - type ToastActionElement, - ToastProvider, - ToastViewport, - Toast, - ToastTitle, - ToastDescription, - ToastClose, - ToastAction, -} diff --git a/pages/src/components/ui/toaster.tsx b/pages/src/components/ui/toaster.tsx deleted file mode 100644 index 171beb46..00000000 --- a/pages/src/components/ui/toaster.tsx +++ /dev/null @@ -1,35 +0,0 @@ -"use client" - -import { useToast } from "@/hooks/use-toast" -import { - Toast, - ToastClose, - ToastDescription, - ToastProvider, - ToastTitle, - ToastViewport, -} from "@/components/ui/toast" - -export function Toaster() { - const { toasts } = useToast() - - return ( - - {toasts.map(function ({ id, title, description, action, ...props }) { - return ( - -
- {title && {title}} - {description && ( - {description} - )} -
- {action} - -
- ) - })} - -
- ) -} diff --git a/pages/src/hooks/use-toast.ts b/pages/src/hooks/use-toast.ts deleted file mode 100644 index 02e111d8..00000000 --- a/pages/src/hooks/use-toast.ts +++ /dev/null @@ -1,194 +0,0 @@ -"use client" - -// Inspired by react-hot-toast library -import * as React from "react" - -import type { - ToastActionElement, - ToastProps, -} from "@/components/ui/toast" - -const TOAST_LIMIT = 1 -const TOAST_REMOVE_DELAY = 1000000 - -type ToasterToast = ToastProps & { - id: string - title?: React.ReactNode - description?: React.ReactNode - action?: ToastActionElement -} - -const actionTypes = { - ADD_TOAST: "ADD_TOAST", - UPDATE_TOAST: "UPDATE_TOAST", - DISMISS_TOAST: "DISMISS_TOAST", - REMOVE_TOAST: "REMOVE_TOAST", -} as const - -let count = 0 - -function genId() { - count = (count + 1) % Number.MAX_SAFE_INTEGER - return count.toString() -} - -type ActionType = typeof actionTypes - -type Action = - | { - type: ActionType["ADD_TOAST"] - toast: ToasterToast - } - | { - type: ActionType["UPDATE_TOAST"] - toast: Partial - } - | { - type: ActionType["DISMISS_TOAST"] - toastId?: ToasterToast["id"] - } - | { - type: ActionType["REMOVE_TOAST"] - toastId?: ToasterToast["id"] - } - -interface State { - toasts: ToasterToast[] -} - -const toastTimeouts = new Map>() - -const addToRemoveQueue = (toastId: string) => { - if (toastTimeouts.has(toastId)) { - return - } - - const timeout = setTimeout(() => { - toastTimeouts.delete(toastId) - dispatch({ - type: "REMOVE_TOAST", - toastId: toastId, - }) - }, TOAST_REMOVE_DELAY) - - toastTimeouts.set(toastId, timeout) -} - -export const reducer = (state: State, action: Action): State => { - switch (action.type) { - case "ADD_TOAST": - return { - ...state, - toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), - } - - case "UPDATE_TOAST": - return { - ...state, - toasts: state.toasts.map((t) => - t.id === action.toast.id ? { ...t, ...action.toast } : t - ), - } - - case "DISMISS_TOAST": { - const { toastId } = action - - // ! Side effects ! - This could be extracted into a dismissToast() action, - // but I'll keep it here for simplicity - if (toastId) { - addToRemoveQueue(toastId) - } else { - state.toasts.forEach((toast) => { - addToRemoveQueue(toast.id) - }) - } - - return { - ...state, - toasts: state.toasts.map((t) => - t.id === toastId || toastId === undefined - ? { - ...t, - open: false, - } - : t - ), - } - } - case "REMOVE_TOAST": - if (action.toastId === undefined) { - return { - ...state, - toasts: [], - } - } - return { - ...state, - toasts: state.toasts.filter((t) => t.id !== action.toastId), - } - } -} - -const listeners: Array<(state: State) => void> = [] - -let memoryState: State = { toasts: [] } - -function dispatch(action: Action) { - memoryState = reducer(memoryState, action) - listeners.forEach((listener) => { - listener(memoryState) - }) -} - -type Toast = Omit - -function toast({ ...props }: Toast) { - const id = genId() - - const update = (props: ToasterToast) => - dispatch({ - type: "UPDATE_TOAST", - toast: { ...props, id }, - }) - const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) - - dispatch({ - type: "ADD_TOAST", - toast: { - ...props, - id, - open: true, - onOpenChange: (open) => { - if (!open) dismiss() - }, - }, - }) - - return { - id: id, - dismiss, - update, - } -} - -function useToast() { - const [state, setState] = React.useState(memoryState) - - React.useEffect(() => { - listeners.push(setState) - return () => { - const index = listeners.indexOf(setState) - if (index > -1) { - listeners.splice(index, 1) - } - } - }, [state]) - - return { - ...state, - toast, - dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), - } -} - -export { useToast, toast } From 866a15d763179cb9a8b2e7fcd3656466c56e6ab5 Mon Sep 17 00:00:00 2001 From: ujiro99 Date: Thu, 15 May 2025 23:30:43 +0900 Subject: [PATCH 10/10] Fix: Lint error. --- src/hooks/useSetting.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/hooks/useSetting.ts b/src/hooks/useSetting.ts index a3874545..7ecf35a8 100644 --- a/src/hooks/useSetting.ts +++ b/src/hooks/useSetting.ts @@ -29,6 +29,8 @@ const emptySettings: SettingsType = { userStyles: [], startupMethod: { method: STARTUP_METHOD.TEXT_SELECTION }, stars: [], + commandExecutionCount: 0, + hasShownReviewRequest: false, } export function useSetting(): useSettingReturn {