From 47fb7482041e5c0f956b2a88f2c02046e10548be Mon Sep 17 00:00:00 2001 From: zenparsing Date: Mon, 28 Oct 2024 17:37:25 -0400 Subject: [PATCH] [NTP Next] Add updated NTP with background support --- .storybook/main.ts | 6 +- browser/about_flags.cc | 7 + browser/brave_browser_features.cc | 4 + browser/brave_browser_features.h | 1 + browser/brave_content_browser_client.cc | 4 + browser/resources/brave_new_tab/BUILD.gn | 25 ++ .../brave_new_tab/components/app.style.ts | 94 +++++ .../brave_new_tab/components/app.tsx | 43 +++ .../components/background.style.ts | 47 +++ .../brave_new_tab/components/background.tsx | 68 ++++ .../components/background_caption.style.ts | 47 +++ .../components/background_caption.tsx | 84 +++++ .../components/locale_context.tsx | 59 ++++ .../components/new_tab_context.tsx | 32 ++ .../settings/background_panel.style.ts | 113 ++++++ .../components/settings/background_panel.tsx | 196 +++++++++++ .../settings/background_type_panel.tsx | 143 ++++++++ .../settings/settings_modal.style.ts | 39 +++ .../components/settings/settings_modal.tsx | 78 +++++ .../brave_new_tab/lib/image_loader.ts | 38 ++ .../brave_new_tab/lib/inline_css_vars.ts | 14 + .../brave_new_tab/lib/locale_strings.ts | 20 ++ .../resources/brave_new_tab/lib/scoped_css.ts | 58 ++++ browser/resources/brave_new_tab/lib/store.ts | 88 +++++ .../brave_new_tab/lib/url_sanitizer.ts | 17 + .../brave_new_tab/lib/use_model_state.ts | 35 ++ .../brave_new_tab/models/backgrounds.ts | 99 ++++++ .../brave_new_tab/models/new_tab_model.ts | 92 +++++ .../resources/brave_new_tab/new_tab_page.html | 22 ++ .../resources/brave_new_tab/new_tab_page.tsx | 32 ++ .../resources/brave_new_tab/stories/index.tsx | 29 ++ .../brave_new_tab/stories/sb_locale.ts | 34 ++ .../brave_new_tab/stories/sb_new_tab_model.ts | 104 ++++++ browser/resources/brave_new_tab/tsconfig.json | 8 + .../brave_new_tab/webui/callback_listeners.ts | 25 ++ .../brave_new_tab/webui/new_tab_page_proxy.ts | 35 ++ .../brave_new_tab/webui/webui_locale.ts | 18 + .../webui/webui_new_tab_model.ts | 153 ++++++++ browser/sources.gni | 8 + browser/ui/config.gni | 6 +- browser/ui/webui/brave_new_tab/BUILD.gn | 51 +++ .../webui/brave_new_tab/background_adapter.cc | 328 ++++++++++++++++++ .../webui/brave_new_tab/background_adapter.h | 71 ++++ .../brave_new_tab/custom_image_chooser.cc | 86 +++++ .../brave_new_tab/custom_image_chooser.h | 55 +++ .../ui/webui/brave_new_tab/new_tab_page.mojom | 94 +++++ .../brave_new_tab/new_tab_page_handler.cc | 148 ++++++++ .../brave_new_tab/new_tab_page_handler.h | 79 +++++ .../ui/webui/brave_new_tab/new_tab_page_ui.cc | 118 +++++++ .../ui/webui/brave_new_tab/new_tab_page_ui.h | 39 +++ .../ui/webui/brave_new_tab/update_observer.cc | 52 +++ .../ui/webui/brave_new_tab/update_observer.h | 45 +++ .../webui/brave_web_ui_controller_factory.cc | 5 + browser/ui/webui/new_tab_page/DEPS | 2 +- .../ui/webui/new_tab_page/brave_new_tab_ui.cc | 4 +- components/brave_new_tab_ui/BUILD.gn | 8 +- .../brave_new_tab_ui/brave_new_tab.html | 2 +- components/resources/BUILD.gn | 4 +- .../resources/brave_components_resources.grd | 1 + .../resources/brave_components_strings.grd | 1 + .../resources/brave_new_tab_resources.grdp | 6 + .../resources/brave_new_tab_strings.grdp | 47 +++ resources/resource_ids.spec | 6 +- ui/webui/resources/BUILD.gn | 1 + 64 files changed, 3265 insertions(+), 13 deletions(-) create mode 100644 browser/resources/brave_new_tab/BUILD.gn create mode 100644 browser/resources/brave_new_tab/components/app.style.ts create mode 100644 browser/resources/brave_new_tab/components/app.tsx create mode 100644 browser/resources/brave_new_tab/components/background.style.ts create mode 100644 browser/resources/brave_new_tab/components/background.tsx create mode 100644 browser/resources/brave_new_tab/components/background_caption.style.ts create mode 100644 browser/resources/brave_new_tab/components/background_caption.tsx create mode 100644 browser/resources/brave_new_tab/components/locale_context.tsx create mode 100644 browser/resources/brave_new_tab/components/new_tab_context.tsx create mode 100644 browser/resources/brave_new_tab/components/settings/background_panel.style.ts create mode 100644 browser/resources/brave_new_tab/components/settings/background_panel.tsx create mode 100644 browser/resources/brave_new_tab/components/settings/background_type_panel.tsx create mode 100644 browser/resources/brave_new_tab/components/settings/settings_modal.style.ts create mode 100644 browser/resources/brave_new_tab/components/settings/settings_modal.tsx create mode 100644 browser/resources/brave_new_tab/lib/image_loader.ts create mode 100644 browser/resources/brave_new_tab/lib/inline_css_vars.ts create mode 100644 browser/resources/brave_new_tab/lib/locale_strings.ts create mode 100644 browser/resources/brave_new_tab/lib/scoped_css.ts create mode 100644 browser/resources/brave_new_tab/lib/store.ts create mode 100644 browser/resources/brave_new_tab/lib/url_sanitizer.ts create mode 100644 browser/resources/brave_new_tab/lib/use_model_state.ts create mode 100644 browser/resources/brave_new_tab/models/backgrounds.ts create mode 100644 browser/resources/brave_new_tab/models/new_tab_model.ts create mode 100644 browser/resources/brave_new_tab/new_tab_page.html create mode 100644 browser/resources/brave_new_tab/new_tab_page.tsx create mode 100644 browser/resources/brave_new_tab/stories/index.tsx create mode 100644 browser/resources/brave_new_tab/stories/sb_locale.ts create mode 100644 browser/resources/brave_new_tab/stories/sb_new_tab_model.ts create mode 100644 browser/resources/brave_new_tab/tsconfig.json create mode 100644 browser/resources/brave_new_tab/webui/callback_listeners.ts create mode 100644 browser/resources/brave_new_tab/webui/new_tab_page_proxy.ts create mode 100644 browser/resources/brave_new_tab/webui/webui_locale.ts create mode 100644 browser/resources/brave_new_tab/webui/webui_new_tab_model.ts create mode 100644 browser/ui/webui/brave_new_tab/BUILD.gn create mode 100644 browser/ui/webui/brave_new_tab/background_adapter.cc create mode 100644 browser/ui/webui/brave_new_tab/background_adapter.h create mode 100644 browser/ui/webui/brave_new_tab/custom_image_chooser.cc create mode 100644 browser/ui/webui/brave_new_tab/custom_image_chooser.h create mode 100644 browser/ui/webui/brave_new_tab/new_tab_page.mojom create mode 100644 browser/ui/webui/brave_new_tab/new_tab_page_handler.cc create mode 100644 browser/ui/webui/brave_new_tab/new_tab_page_handler.h create mode 100644 browser/ui/webui/brave_new_tab/new_tab_page_ui.cc create mode 100644 browser/ui/webui/brave_new_tab/new_tab_page_ui.h create mode 100644 browser/ui/webui/brave_new_tab/update_observer.cc create mode 100644 browser/ui/webui/brave_new_tab/update_observer.h create mode 100644 components/resources/brave_new_tab_resources.grdp create mode 100644 components/resources/brave_new_tab_strings.grdp diff --git a/.storybook/main.ts b/.storybook/main.ts index c201a222ea00..1d022664c8e9 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -22,7 +22,11 @@ const slashStoriesIndexer: Indexer = { const config: StorybookConfig = { stories: process.env.STORYBOOK_STORYPATH ? [`../${process.env.STORYBOOK_STORYPATH}`] - : ['../components/**/stories/*.tsx', '../components/**/*.stories.tsx'], + : [ + '../components/**/stories/*.tsx', + '../components/**/*.stories.tsx', + '../browser/resources/**/stories/*.tsx' + ], typescript: { check: false, reactDocgen: false, diff --git a/browser/about_flags.cc b/browser/about_flags.cc index ec5befc05f5a..c37cc21c50d6 100644 --- a/browser/about_flags.cc +++ b/browser/about_flags.cc @@ -501,6 +501,13 @@ kOsDesktop, \ FEATURE_VALUE_TYPE(features::kBraveNtpSearchWidget), \ }, \ + { \ + "brave-use-updated-ntp", \ + "Use the updated New Tab Page", \ + "Uses an updated version of the New Tab Page", \ + kOsDesktop, \ + FEATURE_VALUE_TYPE(features::kUseUpdatedNTP), \ + }, \ { \ "brave-adblock-cname-uncloaking", \ "Enable CNAME uncloaking", \ diff --git a/browser/brave_browser_features.cc b/browser/brave_browser_features.cc index 85c244faad08..8e3ca0b1dd30 100644 --- a/browser/brave_browser_features.cc +++ b/browser/brave_browser_features.cc @@ -9,6 +9,10 @@ namespace features { +BASE_FEATURE(kUseUpdatedNTP, + "BraveUseUpdatedNewTabPage", + base::FEATURE_DISABLED_BY_DEFAULT); + // Cleanup Session Cookies on browser restart if Session Restore is enabled. BASE_FEATURE(kBraveCleanupSessionCookiesOnSessionRestore, "BraveCleanupSessionCookiesOnSessionRestore", diff --git a/browser/brave_browser_features.h b/browser/brave_browser_features.h index ad30895a251e..5d06cd360b4f 100644 --- a/browser/brave_browser_features.h +++ b/browser/brave_browser_features.h @@ -13,6 +13,7 @@ namespace features { +BASE_DECLARE_FEATURE(kUseUpdatedNTP); BASE_DECLARE_FEATURE(kBraveCleanupSessionCookiesOnSessionRestore); BASE_DECLARE_FEATURE(kBraveCopyCleanLinkByDefault); BASE_DECLARE_FEATURE(kBraveCopyCleanLinkFromJs); diff --git a/browser/brave_content_browser_client.cc b/browser/brave_content_browser_client.cc index 7302bceddb59..0fc6ff64d201 100644 --- a/browser/brave_content_browser_client.cc +++ b/browser/brave_content_browser_client.cc @@ -229,6 +229,7 @@ using extensions::ChromeContentBrowserClientExtensionsPart; #if !BUILDFLAG(IS_ANDROID) #include "brave/browser/new_tab/new_tab_shows_navigation_throttle.h" #include "brave/browser/ui/geolocation/brave_geolocation_permission_tab_helper.h" +#include "brave/browser/ui/webui/brave_new_tab/new_tab_page_ui.h" #include "brave/browser/ui/webui/brave_news_internals/brave_news_internals_ui.h" #include "brave/browser/ui/webui/brave_rewards/rewards_page_top_ui.h" #include "brave/browser/ui/webui/brave_rewards/rewards_panel_ui.h" @@ -867,6 +868,9 @@ void BraveContentBrowserClient::RegisterBrowserInterfaceBindersForFrame( content::RegisterWebUIControllerInterfaceBinder< commands::mojom::CommandsService, BraveSettingsUI>(map); } + content::RegisterWebUIControllerInterfaceBinder< + brave_new_tab::mojom::NewTabPageHandler, brave_new_tab::NewTabPageUI>( + map); #endif auto* prefs = diff --git a/browser/resources/brave_new_tab/BUILD.gn b/browser/resources/brave_new_tab/BUILD.gn new file mode 100644 index 000000000000..26113937495b --- /dev/null +++ b/browser/resources/brave_new_tab/BUILD.gn @@ -0,0 +1,25 @@ +# Copyright (c) 2025 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. + +import("//brave/components/common/typescript.gni") +import("//mojo/public/tools/bindings/mojom.gni") + +assert(!is_android) + +transpile_web_ui("resources") { + entry_points = [ [ + "new_tab", + rebase_path("new_tab_page.tsx"), + ] ] + resource_name = "brave_new_tab" + output_module = true + deps = [ "//brave/browser/ui/webui/brave_new_tab:mojom_js" ] +} + +pack_web_resources("generated_resources") { + resource_name = "brave_new_tab" + output_dir = "$root_gen_dir/brave/browser/resources/brave_new_tab" + deps = [ ":resources" ] +} diff --git a/browser/resources/brave_new_tab/components/app.style.ts b/browser/resources/brave_new_tab/components/app.style.ts new file mode 100644 index 000000000000..fcf63a977ef2 --- /dev/null +++ b/browser/resources/brave_new_tab/components/app.style.ts @@ -0,0 +1,94 @@ +/* Copyright (c) 2025 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import { color, font } from '@brave/leo/tokens/css/variables' +import { scoped, global } from '../lib/scoped_css' + +export const style = scoped.css` + + .settings { + --leo-icon-size: 20px; + + position: absolute; + inset-block-start: 4px; + inset-inline-end: 4px; + block-size: 20px; + inline-size: 20px; + opacity: 0.5; + color: #fff; + filter: drop-shadow(0px 1px 4px rgba(0, 0, 0, 0.60)); + + &:hover { + opacity: 0.7; + cursor: pointer; + } + } + + main { + display: flex; + flex-direction: column; + align-items: center; + min-height: 100vh; + padding-top: 40px; + } + + .topsites-container { + min-height: 32px; + } + + .searchbox-container { + flex: 1 1 auto; + margin: 16px 0; + } + + .background-caption-container { + margin: 8px 0; + } + + .widget-container { + min-height: 8px; + } + +` + +global.css` + @scope (${style.selector}) { + + & { + font: ${font.default.regular}; + color: ${color.text.primary}; + } + + button { + margin: 0; + padding: 0; + background: 0; + border: none; + text-align: unset; + width: unset; + font: inherit; + cursor: pointer; + + &:disabled { + cursor: default; + } + } + + h2 { + font: ${font.heading.h2}; + margin: 0; + } + + h3 { + font: ${font.heading.h3}; + margin: 0; + } + + h4 { + font: ${font.heading.h4}; + margin: 0; + } + } +` diff --git a/browser/resources/brave_new_tab/components/app.tsx b/browser/resources/brave_new_tab/components/app.tsx new file mode 100644 index 000000000000..17aca3fa15b3 --- /dev/null +++ b/browser/resources/brave_new_tab/components/app.tsx @@ -0,0 +1,43 @@ +/* Copyright (c) 2025 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import * as React from 'react' +import Icon from '@brave/leo/react/icon' + +import { Background } from './background' +import { BackgroundCaption } from './background_caption' +import { SettingsModal, SettingsView } from './settings/settings_modal' + +import { style } from './app.style' + +export function App() { + const [settingsView, setSettingsView] = + React.useState(null) + + return ( +
+ +
+
+
+
+ +
+
+
+ + setSettingsView(null)} + /> +
+ ) +} diff --git a/browser/resources/brave_new_tab/components/background.style.ts b/browser/resources/brave_new_tab/components/background.style.ts new file mode 100644 index 000000000000..13bb8486f602 --- /dev/null +++ b/browser/resources/brave_new_tab/components/background.style.ts @@ -0,0 +1,47 @@ +/* Copyright (c) 2025 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import { scoped } from '../lib/scoped_css' + +export const style = scoped.css` + + & { + pointer-events: none; + position: fixed; + inset: 0; + z-index: -1; + display: flex; + animation-name: fade-in; + animation-timing-function: ease-in-out; + animation-duration: 350ms; + animation-delay: 0s; + animation-fill-mode: both; + + > div { + flex: 1 1 auto; + background-size: cover; + background-repeat: no-repeat; + background-position: center center; + } + } + + .image-background { + background: + linear-gradient( + rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0) 35%, rgba(0, 0, 0, 0) 80%, + rgba(0, 0, 0, 0.6) 100%), + var(--ntp-background); + } + + .color-background { + background: var(--ntp-background); + } + + @keyframes fade-in { + from { opacity: 0; } + to { opacity: 1; } + } + +` diff --git a/browser/resources/brave_new_tab/components/background.tsx b/browser/resources/brave_new_tab/components/background.tsx new file mode 100644 index 000000000000..94ab020acc20 --- /dev/null +++ b/browser/resources/brave_new_tab/components/background.tsx @@ -0,0 +1,68 @@ +/* Copyright (c) 2025 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import * as React from 'react' + +import { useNewTabState } from './new_tab_context' +import { loadImage } from '../lib/image_loader' + +import { style } from './background.style' + +function setBackgroundVariable(value: string) { + if (value) { + document.body.style.setProperty('--ntp-background', value) + } else { + document.body.style.removeProperty('--ntp-background') + } +} + +function ImageBackground(props: { url: string }) { + // In order to avoid a "flash-of-unloaded-image", load the image in the + // background and only update the background CSS variable when the image has + // finished loading. + React.useEffect(() => { + loadImage(props.url).then((loaded) => { + if (loaded) { + setBackgroundVariable(`url(${CSS.escape(props.url)})`) + } + }) + }, [props.url]) + + return
+} + +function ColorBackground(props: { colorValue: string }) { + React.useEffect(() => { + setBackgroundVariable(props.colorValue) + }, [props.colorValue]) + + return
+} + +export function Background() { + const currentBackground = useNewTabState((state) => state.currentBackground) + + function renderBackground() { + if (!currentBackground) { + return + } + + switch (currentBackground.type) { + case 'brave': + case 'custom': + case 'sponsored': + return + case 'solid': + case 'gradient': + return + } + } + + return ( +
+ {renderBackground()} +
+ ) +} diff --git a/browser/resources/brave_new_tab/components/background_caption.style.ts b/browser/resources/brave_new_tab/components/background_caption.style.ts new file mode 100644 index 000000000000..45446fedec98 --- /dev/null +++ b/browser/resources/brave_new_tab/components/background_caption.style.ts @@ -0,0 +1,47 @@ +/* Copyright (c) 2025 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import { color, font } from '@brave/leo/tokens/css/variables' +import { scoped } from '../lib/scoped_css' + +export const style = scoped.css` + a { + text-decoration: none; + color: inherit; + } + + .photo-credits { + color: ${color.white}; + font: ${font.xSmall.regular}; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.10); + opacity: .5; + } + + .sponsored-logo { + --leo-icon-size: 20px; + + display: flex; + flex-direction: column; + align-items: end; + color: ${color.white}; + + leo-icon { + opacity: 0; + transition: opacity 200ms; + } + + img { + margin: 2px 20px 0 20px; + width: 170px; + height: auto; + } + + &:hover { + leo-icon { + opacity: .7; + } + } + } +` diff --git a/browser/resources/brave_new_tab/components/background_caption.tsx b/browser/resources/brave_new_tab/components/background_caption.tsx new file mode 100644 index 000000000000..e430abd74df9 --- /dev/null +++ b/browser/resources/brave_new_tab/components/background_caption.tsx @@ -0,0 +1,84 @@ +/* Copyright (c) 2025 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import * as React from 'react' +import Icon from '@brave/leo/react/icon' + +import formatMessage from '$web-common/formatMessage' +import { sanitizeExternalURL } from '../lib/url_sanitizer' +import { useLocale } from './locale_context' +import { useNewTabState } from './new_tab_context' + +import { + BraveBackground, + SponsoredImageBackground } from '../models/new_tab_model' + +import { style } from './background_caption.style' + +function BraveBackgroundCredits( + props: { background: BraveBackground} +) { + const { getString } = useLocale() + const { author, link } = props.background + if (!author) { + return null + } + const text = formatMessage(getString('photoCreditsText'), [author]) + const sanitizedLink = sanitizeExternalURL(link) + if (!sanitizedLink) { + return {text} + } + return ( + + {formatMessage(getString('photoCreditsText'), [author])} + + ) +} + +function SponsoredBackgroundLogo( + props: { background: SponsoredImageBackground } +) { + if (!props.background.logo) { + return null + } + const { logo } = props.background + return ( + + + {logo.alt} + + ) +} + +export function BackgroundCaption() { + const currentBackground = useNewTabState((state) => state.currentBackground) + + function renderCaption() { + switch (currentBackground?.type) { + case 'brave': + return + case 'sponsored': + return + default: + return null + } + } + + return ( +
+ {renderCaption()} +
+ ) +} diff --git a/browser/resources/brave_new_tab/components/locale_context.tsx b/browser/resources/brave_new_tab/components/locale_context.tsx new file mode 100644 index 000000000000..19b877d8bc69 --- /dev/null +++ b/browser/resources/brave_new_tab/components/locale_context.tsx @@ -0,0 +1,59 @@ +/* Copyright (c) 2025 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import * as React from 'react' + +import { StringKey } from '../lib/locale_strings' + +export interface Locale { + getString: (key: StringKey) => string + getPluralString: (key: StringKey, count: number) => Promise +} + +const Context = React.createContext({ + getString: () => '', + getPluralString: async () => '' +}) + +interface Props { + locale: Locale + children: React.ReactNode +} + +export function LocaleContext(props: Props) { + return ( + + {props.children} + + ) +} + +export function useLocale(): Locale { + return React.useContext(Context) +} + +export function usePluralString( + key: StringKey, + count: number | undefined | null +) { + const locale = useLocale() + const [value, setValue] = React.useState('') + + React.useEffect(() => { + if (typeof count !== 'number') { + setValue('') + return + } + let canUpdate = true + locale.getPluralString(key, count).then((newValue) => { + if (canUpdate) { + setValue(newValue) + } + }) + return () => { canUpdate = false } + }, [locale, count]) + + return value +} diff --git a/browser/resources/brave_new_tab/components/new_tab_context.tsx b/browser/resources/brave_new_tab/components/new_tab_context.tsx new file mode 100644 index 000000000000..5408d5e7424f --- /dev/null +++ b/browser/resources/brave_new_tab/components/new_tab_context.tsx @@ -0,0 +1,32 @@ +/* Copyright (c) 2025 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import * as React from 'react' + +import { NewTabModel, NewTabState, defaultModel } from '../models/new_tab_model' +import { useModelState } from '../lib/use_model_state' + +const Context = React.createContext(defaultModel()) + +interface Props { + model: NewTabModel + children: React.ReactNode +} + +export function NewTabContext(props: Props) { + return ( + + {props.children} + + ) +} + +export function useNewTabModel(): NewTabModel { + return React.useContext(Context) +} + +export function useNewTabState(map: (state: NewTabState) => T): T { + return useModelState(useNewTabModel(), map) +} diff --git a/browser/resources/brave_new_tab/components/settings/background_panel.style.ts b/browser/resources/brave_new_tab/components/settings/background_panel.style.ts new file mode 100644 index 000000000000..a3e963551e9d --- /dev/null +++ b/browser/resources/brave_new_tab/components/settings/background_panel.style.ts @@ -0,0 +1,113 @@ +/* Copyright (c) 2025 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import { color, font } from '@brave/leo/tokens/css/variables' +import { scoped } from '../../lib/scoped_css' + +export const style = scoped.css` + & { + display: flex; + flex-direction: column; + gap: 16px; + } + + .toggle-row { + display: flex; + align-items: center; + + label { + flex: 1 1 auto; + } + } + + .background-options { + display: flex; + flex-wrap: wrap; + gap: 16px; + + button { + display: flex; + flex-direction: column; + gap: 8px; + } + } + + .preview { + background: var(--preview-background, ${color.container.highlight}); + background-size: cover; + background-repeat: no-repeat; + background-position: center center; + border-radius: 10px; + width: 198px; + height: 156px; + } + + .background-option { + position: relative; + + &:hover .remove-image { + visibility: visible; + } + } + + .selected-marker { + --leo-icon-color: #fff; + --leo-icon-size: 24px; + + position: absolute; + inset-block-start: 10px; + inset-inline-end: 10px; + background: ${color.icon.interactive}; + border-radius: 50%; + padding: 6px; + + + .allow-remove:hover & { + visibility: hidden; + } + } + + .remove-image { + --leo-icon-size: 24px; + + position: absolute; + inset-block-start: 10px; + inset-inline-end: 10px; + background-color: #fff; + border-radius: 50%; + box-shadow: rgba(0, 0, 0, 0.5) 0px 0px 5px; + padding: 6px; + visibility: hidden; + + &:hover { + color: ${color.icon.interactive}; + } + } + + .upload { + --leo-icon-size: 36px; + --leo-progressring-size: 36px; + + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 16px; + border: solid 2px ${color.divider.subtle}; + font: ${font.small.regular}; + } + + h4 button { + --leo-icon-size: 20px; + + display: flex; + align-items: center; + gap: 4px; + + &:hover { + color: ${color.text.interactive}; + } + } +` diff --git a/browser/resources/brave_new_tab/components/settings/background_panel.tsx b/browser/resources/brave_new_tab/components/settings/background_panel.tsx new file mode 100644 index 000000000000..9641d83f1c6a --- /dev/null +++ b/browser/resources/brave_new_tab/components/settings/background_panel.tsx @@ -0,0 +1,196 @@ +/* Copyright (c) 2025 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import * as React from 'react' +import Icon from '@brave/leo/react/icon' +import ProgressRing from '@brave/leo/react/progressRing' +import Toggle from '@brave/leo/react/toggle' + +import { BackgroundType } from '../../models/new_tab_model' +import { useNewTabModel, useNewTabState } from '../new_tab_context' +import { useLocale } from '../locale_context' +import { inlineCSSVars } from '../../lib/inline_css_vars' +import { BackgroundTypePanel } from './background_type_panel' + +import { + backgroundCSSValue, + gradientPreviewBackground, + solidPreviewBackground } from '../../models/backgrounds' + +import { style } from './background_panel.style' + +export function BackgroundPanel() { + const { getString } = useLocale() + const model = useNewTabModel() + + const [ + backgroundsEnabled, + backgroundsCustomizable, + sponsoredImagesEnabled, + selectedBackgroundType, + selectedBackground, + braveBackgrounds, + customBackgrounds + ] = useNewTabState((state) => [ + state.backgroundsEnabled, + state.backgroundsCustomizable, + state.sponsoredImagesEnabled, + state.selectedBackgroundType, + state.selectedBackground, + state.braveBackgrounds, + state.customBackgrounds + ]) + + const [panelType, setPanelType] = React.useState('none') + const [uploading, setUploading] = React.useState(false) + + React.useEffect(() => { + setUploading(false) + }, [selectedBackground, customBackgrounds]) + + function getTypePreviewValue(type: BackgroundType) { + const isSelectedType = type === selectedBackgroundType + switch (type) { + case 'brave': + return braveBackgrounds[0]?.imageUrl ?? '' + case 'custom': + if (isSelectedType && selectedBackground) { + return selectedBackground + } + return customBackgrounds[0] ?? '' + case 'solid': + if (isSelectedType && selectedBackground) { + return selectedBackground + } + return solidPreviewBackground + case 'gradient': + if (isSelectedType && selectedBackground) { + return selectedBackground + } + return gradientPreviewBackground + case 'none': + return '' + } + } + + function renderUploadPreview() { + return ( +
+ {uploading ? : } + {getString('uploadBackgroundLabel')} +
+ ) + } + + function renderTypePreview(type: BackgroundType) { + if (type === 'custom' && customBackgrounds.length === 0) { + return renderUploadPreview() + } + return ( +
+ { + type === selectedBackgroundType && + + + + } +
+ ) + } + + function showCustomBackgroundChooser() { + model.showCustomBackgroundChooser().then((backgroundSelected) => { + if (backgroundSelected) { + setUploading(true) + } + }) + } + + function onCustomPreviewClick() { + if (customBackgrounds.length === 0) { + showCustomBackgroundChooser() + } else { + setPanelType('custom') + } + } + + if (panelType !== 'none') { + return ( +
+ ( + + )} + onClose={() => { setPanelType('none') }} + /> +
+ ) + } + + return ( +
+
+ + { model.setBackgroundsEnabled(checked) }} + /> +
+ { + backgroundsEnabled && backgroundsCustomizable && <> +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + } + { + backgroundsEnabled && +
+ + { + model.setSponsoredImagesEnabled(checked) + }} + /> +
+ } +
+ ) +} diff --git a/browser/resources/brave_new_tab/components/settings/background_type_panel.tsx b/browser/resources/brave_new_tab/components/settings/background_type_panel.tsx new file mode 100644 index 000000000000..291fb30ae982 --- /dev/null +++ b/browser/resources/brave_new_tab/components/settings/background_type_panel.tsx @@ -0,0 +1,143 @@ +/* Copyright (c) 2025 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import * as React from 'react' +import Icon from '@brave/leo/react/icon' +import Toggle from '@brave/leo/react/toggle' + +import { BackgroundType } from '../../models/new_tab_model' +import { useNewTabModel, useNewTabState } from '../new_tab_context' +import { useLocale } from '../locale_context' +import { inlineCSSVars } from '../../lib/inline_css_vars' +import classNames from '$web-common/classnames' + +import { + backgroundCSSValue, + solidBackgrounds, + gradientBackgrounds } from '../../models/backgrounds' + +interface Props { + backgroundType: BackgroundType + renderUploadOption: () => React.ReactNode + onClose: () => void +} + +export function BackgroundTypePanel(props: Props) { + const { getString } = useLocale() + const model = useNewTabModel() + + const [ + selectedBackgroundType, + selectedBackground, + customBackgrounds, + currentBackground + ] = useNewTabState((state) => [ + state.selectedBackgroundType, + state.selectedBackground, + state.customBackgrounds, + state.currentBackground + ]) + + const type = props.backgroundType + + function panelTitle() { + switch (type) { + case 'custom': return getString('customBackgroundTitle') + case 'gradient': return getString('gradientBackgroundTitle') + case 'solid': return getString('solidBackgroundTitle') + default: return '' + } + } + + function panelValues() { + switch (type) { + case 'custom': return customBackgrounds + case 'gradient': return gradientBackgrounds + case 'solid': return solidBackgrounds + default: return [] + } + } + + function onRandomizeToggle(detail: { checked: boolean }) { + if (detail.checked) { + model.selectBackground(type, '') + } else if (currentBackground) { + switch (currentBackground.type) { + case 'custom': + model.selectBackground(type, currentBackground.imageUrl) + break + case 'solid': + case 'gradient': + model.selectBackground(type, currentBackground.cssValue) + break + default: + break + } + } + } + + const values = panelValues() + + return <> +

+ +

+
+ + +
+
+ {values.map((value) => { + const isSelected = + selectedBackgroundType === type && + selectedBackground === value + + return ( +
+ + { + type === 'custom' && + + } +
+ ) + })} + {type === 'custom' && props.renderUploadOption()} +
+ +} diff --git a/browser/resources/brave_new_tab/components/settings/settings_modal.style.ts b/browser/resources/brave_new_tab/components/settings/settings_modal.style.ts new file mode 100644 index 000000000000..0996e9df8610 --- /dev/null +++ b/browser/resources/brave_new_tab/components/settings/settings_modal.style.ts @@ -0,0 +1,39 @@ +/* Copyright (c) 2025 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import { color } from '@brave/leo/tokens/css/variables' +import { scoped } from '../../lib/scoped_css' + +export const style = scoped.css` + & { + --leo-dialog-width: 720px; + --leo-dialog-padding: 0; + --leo-dialog-background: ${color.container.background}; + + height: 0; + } + + h3 { + margin: 24px 24px 16px; + } + + .panel-body { + display: flex; + gap: 16px; + } + + nav { + flex: 0 0 244px; + white-space: nowrap; + } + + section { + flex: 1 1 auto; + padding: 10px 16px 16px; + height: 360px; + overflow: auto; + overscroll-behavior: contain; + } +` diff --git a/browser/resources/brave_new_tab/components/settings/settings_modal.tsx b/browser/resources/brave_new_tab/components/settings/settings_modal.tsx new file mode 100644 index 000000000000..c009a09e9c8c --- /dev/null +++ b/browser/resources/brave_new_tab/components/settings/settings_modal.tsx @@ -0,0 +1,78 @@ +/* Copyright (c) 2025 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import * as React from 'react' +import Dialog from '@brave/leo/react/dialog' +import Navigation from '@brave/leo/react/navigation' +import NavigationItem from '@brave/leo/react/navigationItem' + +import { BackgroundPanel } from './background_panel' +import { useLocale } from '../locale_context' + +import { style } from './settings_modal.style' + +export type SettingsView = 'background' + +interface Props { + initialView: SettingsView | null + isOpen: boolean + onClose: () => void +} + +export function SettingsModal(props: Props) { + const { getString } = useLocale() + + const [currentView, setCurrentView] = + React.useState(props.initialView || 'background') + + React.useEffect(() => { + if (props.isOpen) { + setCurrentView(props.initialView || 'background') + } + }, [props.isOpen, props.initialView]) + + function renderPanel() { + switch (currentView) { + case 'background': return + } + } + + function getNavItemText(view: SettingsView) { + switch (view) { + case 'background': return getString('backgroundSettingsTitle') + } + } + + function renderNavItem(view: SettingsView) { + return ( + setCurrentView(view)} + > + {getNavItemText(view)} + + ) + } + + return ( +
+ +

+ {getString('settingsTitle')} +

+
+ +
+ {renderPanel()} +
+
+
+
+ ) +} diff --git a/browser/resources/brave_new_tab/lib/image_loader.ts b/browser/resources/brave_new_tab/lib/image_loader.ts new file mode 100644 index 000000000000..a18fc1bcce85 --- /dev/null +++ b/browser/resources/brave_new_tab/lib/image_loader.ts @@ -0,0 +1,38 @@ +/* Copyright (c) 2025 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +export const placeholderImageSrc = + 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg"%3E%3C/svg%3E' + +// Loads an image in the background and resolves when the image has either +// loaded or was unable to load. +export function loadImage(url: string): Promise { + return new Promise((resolve) => { + if (!url) { + resolve(false) + return + } + + const unlisten = () => { + image.removeEventListener('load', onLoad) + image.removeEventListener('error', onError) + } + + const onLoad = () => { + unlisten() + resolve(true) + } + + const onError = () => { + unlisten() + resolve(false) + } + + const image = new Image() + image.addEventListener('load', onLoad) + image.addEventListener('error', onError) + image.src = url + }) +} diff --git a/browser/resources/brave_new_tab/lib/inline_css_vars.ts b/browser/resources/brave_new_tab/lib/inline_css_vars.ts new file mode 100644 index 000000000000..47bdd38f971c --- /dev/null +++ b/browser/resources/brave_new_tab/lib/inline_css_vars.ts @@ -0,0 +1,14 @@ +/* Copyright (c) 2025 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +interface CSSVars { + [key: `--${string}`]: string | number +} + +// Allows a collection of CSS custom variables to be used in the "style" prop of +// React components. +export function inlineCSSVars(vars: CSSVars) { + return vars as React.CSSProperties +} diff --git a/browser/resources/brave_new_tab/lib/locale_strings.ts b/browser/resources/brave_new_tab/lib/locale_strings.ts new file mode 100644 index 000000000000..adfe37af97a0 --- /dev/null +++ b/browser/resources/brave_new_tab/lib/locale_strings.ts @@ -0,0 +1,20 @@ +/* Copyright (c) 2025 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +export type StringKey = + 'backgroundSettingsTitle' | + 'braveBackgroundLabel' | + 'customBackgroundLabel' | + 'customBackgroundTitle' | + 'gradientBackgroundLabel' | + 'gradientBackgroundTitle' | + 'photoCreditsText' | + 'randomizeBackgroundLabel' | + 'settingsTitle' | + 'showBackgroundsLabel' | + 'showSponsoredImagesLabel' | + 'solidBackgroundLabel' | + 'solidBackgroundTitle' | + 'uploadBackgroundLabel' diff --git a/browser/resources/brave_new_tab/lib/scoped_css.ts b/browser/resources/brave_new_tab/lib/scoped_css.ts new file mode 100644 index 000000000000..a2cf55fbfcf4 --- /dev/null +++ b/browser/resources/brave_new_tab/lib/scoped_css.ts @@ -0,0 +1,58 @@ +/* Copyright (c) 2025 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +// Adds CSS to the document. If a stylesheet with the specified `id` has already +// been added to the document, then it will be replaced with the provided CSS. +async function addStyles(cssText: unknown) { + const stylesheet = new CSSStyleSheet() + document.adoptedStyleSheets.push(stylesheet) + await stylesheet.replace(String(cssText)) +} + +const scopeAttributeName = 'data-css-scope' + +class ScopedCSSAttribute { + [scopeAttributeName]: string + + constructor(scopeName: string) { + this[scopeAttributeName] = scopeName + } + + get scope() { + return this[scopeAttributeName] + } + + get selector() { + return `[${scopeAttributeName}=${CSS.escape(this[scopeAttributeName])}]` + } +} + +let nextScopeID = 0x5c09ed; + +// A template tag that adds scoped CSS to the document. The provided CSS text +// is wrapped with a "@scope" at-rule and only applies to elements with a +// "data-css-scope" attribute whose value matches `scopeName`. The CSS rules do +// not apply to any descendant elements that have a "data-css-scope" attribute. +// Returns an object representing the CSS scope data attribute, which can be +// object-spread into a collection of HTML attributes. +export const scoped = { + css(callsite: TemplateStringsArray, ...values: any[]) { + const id = (nextScopeID++).toString(36) + const attr = new ScopedCSSAttribute(id) + addStyles(` + @scope (${attr.selector}) to ([${scopeAttributeName}]) { + ${String.raw(callsite, ...values)} + } + `) + return attr + } +} + +// Adds global CSS to the document. +export const global = { + css(callsite: TemplateStringsArray, ...values: any[]) { + addStyles(String.raw(callsite, ...values)) + } +} diff --git a/browser/resources/brave_new_tab/lib/store.ts b/browser/resources/brave_new_tab/lib/store.ts new file mode 100644 index 000000000000..e049125fdbb4 --- /dev/null +++ b/browser/resources/brave_new_tab/lib/store.ts @@ -0,0 +1,88 @@ +/* Copyright (c) 2025 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +type Listener = (state: State) => void + +type UpdateFunction = (state: State) => Partial + +// A simple object-state store. +export interface Store { + + // Returns the current state of the store. + getState: () => State + + // Updates the state of the store. All listeners will be notified of the state + // change in a future turn of the event loop. + update: (source: Partial | UpdateFunction) => void + + // Adds a listener that will be notified when the state store changes. The + // listener will not be notified immediately. Returns a function that will + // remove the listener from store. + addListener: (listener: Listener) => (() => void) + +} + +export function createStore(initialState: State): Store { + const listeners = new Set>() + const state = { ...initialState } + let notificationQueued = false + + function notify() { + if (notificationQueued) { + return + } + + notificationQueued = true + + // Send update notifications in a future turn in order to avoid reentrancy. + queueMicrotask(() => { + notificationQueued = false + for (const listener of listeners) { + // If a notification has been queued as a result of calling a listener, + // then exit this notification. The next update will be sent in a future + // turn to all remaining listeners. + if (notificationQueued) { + break + } + try { + listener(state) + } catch (e) { + // Rethrow error in a future turn to prevent listeners from + // interfering with each other. + queueMicrotask(() => { throw e }) + } + } + }) + } + + return { + + getState() { + return state + }, + + update(source: Partial | UpdateFunction) { + if (typeof source === 'function') { + source = source(state) + } + for (const [key, value] of Object.entries(source)) { + if (value !== undefined) { + (state as any)[key] = value + } + } + notify() + }, + + addListener(listener: Listener) { + if (!listeners.has(listener)) { + listeners.add(listener) + } + return () => { + listeners.delete(listener) + } + } + + } +} diff --git a/browser/resources/brave_new_tab/lib/url_sanitizer.ts b/browser/resources/brave_new_tab/lib/url_sanitizer.ts new file mode 100644 index 000000000000..b6bade8c8169 --- /dev/null +++ b/browser/resources/brave_new_tab/lib/url_sanitizer.ts @@ -0,0 +1,17 @@ +/* Copyright (c) 2025 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +export function sanitizeExternalURL(urlString: string) { + let url: URL | null = null + try { + url = new URL(urlString) + } catch { + return '' + } + if (url.protocol !== 'https:') { + return '' + } + return url.toString() +} diff --git a/browser/resources/brave_new_tab/lib/use_model_state.ts b/browser/resources/brave_new_tab/lib/use_model_state.ts new file mode 100644 index 000000000000..46b9f3a5d2de --- /dev/null +++ b/browser/resources/brave_new_tab/lib/use_model_state.ts @@ -0,0 +1,35 @@ +/* Copyright (c) 2025 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import * as React from 'react' + +type StateListener = (state: State) => void + +interface Model { + getState: () => State + addListener: (listener: StateListener) => () => void +} + +// A helper for importing model state into component state. This helper can be +// used by model context modules when implementing "useFooState" hooks. +export function useModelState( + model: Model, + map: (state: State) => T +): T { + const [value, setValue] = React.useState(() => map(model.getState())) + React.useEffect(() => { + return model.addListener((state) => { + const result = map(state) + if (result === value) { + // If `map` is the identity function, then call `setState` with a new + // object in order to ensure a re-render. + setValue({ ...result }) + } else { + setValue(result) + } + }) + }, [model]) + return value +} diff --git a/browser/resources/brave_new_tab/models/backgrounds.ts b/browser/resources/brave_new_tab/models/backgrounds.ts new file mode 100644 index 000000000000..4f0217b62012 --- /dev/null +++ b/browser/resources/brave_new_tab/models/backgrounds.ts @@ -0,0 +1,99 @@ +/* Copyright (c) 2025 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import { NewTabState, Background, BackgroundType } from './new_tab_model' + +export const solidBackgrounds = [ + '#5B5C63', '#000000', '#151E9A', '#2197F9', '#1FC3DC', '#086582', '#67D4B4', + '#077D5A', '#3C790B', '#AFCE57', '#F0CB44', '#F28A29', '#FC798F', '#C1226E', + '#FAB5EE', '#C0C4FF', '#9677EE', '#5433B0', '#4A000C' +] + +export const solidPreviewBackground = solidBackgrounds[2] + +export const gradientBackgrounds = [ + 'linear-gradient(125.83deg, #392DD1 0%, #A91B78 99.09%)', + 'linear-gradient(125.83deg, #392DD1 0%, #22B8CF 99.09%)', + 'linear-gradient(90deg, #4F30AB 0.64%, #845EF7 99.36%)', + 'linear-gradient(126.47deg, #A43CE4 16.99%, #A72B6D 86.15%)', + 'radial-gradient(' + + '69.45% 69.45% at 89.46% 81.73%, #641E0C 0%, #500F39 43.54%, #060141 100%)', + 'radial-gradient(80% 80% at 101.61% 76.99%, #2D0264 0%, #030023 100%)', + 'linear-gradient(128.12deg, #43D4D4 6.66%, #1596A9 83.35%)', + 'linear-gradient(323.02deg, #DD7131 18.65%, #FBD460 82.73%)', + 'linear-gradient(128.12deg, #4F86E2 6.66%, #694CD9 83.35%)', + 'linear-gradient(127.39deg, #851B6A 6.04%, #C83553 86.97%)', + 'linear-gradient(130.39deg, #FE6F4C 9.83%, #C53646 85.25%)' +] + +export const gradientPreviewBackground = gradientBackgrounds[0] + +const defaultBackground: Background = { + type: 'gradient', + cssValue: gradientPreviewBackground +} + +function chooseRandom(list: T[]): T | null { + if (list.length === 0) { + return null + } + return list[Math.floor(Math.random() * list.length)] +} + +export function getCurrentBackground(state: NewTabState): Background | null { + const { + backgroundsEnabled, + braveBackgrounds, + customBackgrounds, + selectedBackground, + selectedBackgroundType, + sponsoredImageBackground, + currentBackground } = state + + if (!backgroundsEnabled) { + return defaultBackground + } + + if (sponsoredImageBackground) { + return sponsoredImageBackground + } + + if (currentBackground && + selectedBackgroundType === currentBackground.type && + !selectedBackground) { + return currentBackground + } + + switch (selectedBackgroundType) { + case 'brave': { + return chooseRandom(braveBackgrounds) + } + case 'custom': { + const imageUrl = selectedBackground || chooseRandom(customBackgrounds) + return imageUrl ? { type: 'custom', imageUrl } : null + } + case 'solid': { + const cssValue = selectedBackground || chooseRandom(solidBackgrounds) + return cssValue ? { type: 'solid', cssValue } : null + } + case 'gradient': { + const cssValue = selectedBackground || chooseRandom(gradientBackgrounds) + return cssValue ? { type: 'gradient', cssValue } : null + } + case 'none': { + return defaultBackground + } + } +} + +export function backgroundCSSValue(type: BackgroundType, value: string) { + switch (type) { + case 'brave': + case 'custom': return `url(${CSS.escape(value)})` + case 'solid': + case 'gradient': + case 'none': return value + } +} diff --git a/browser/resources/brave_new_tab/models/new_tab_model.ts b/browser/resources/brave_new_tab/models/new_tab_model.ts new file mode 100644 index 000000000000..923f17339442 --- /dev/null +++ b/browser/resources/brave_new_tab/models/new_tab_model.ts @@ -0,0 +1,92 @@ +/* Copyright (c) 2025 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +export type BackgroundType = 'brave' | 'custom' | 'solid' | 'gradient' | 'none' + +export interface BraveBackground { + type: 'brave' + author: string + imageUrl: string + link: string +} + +export interface ColorBackground { + type: 'solid' | 'gradient' + cssValue: string +} + +export interface CustomBackground { + type: 'custom' + imageUrl: string +} + +export interface SponsoredImageLogo { + alt: string + destinationUrl: string + imageUrl: string +} + +export interface SponsoredImageBackground { + type: 'sponsored' + imageUrl: string, + creativeInstanceId: string + wallpaperId: string + logo: SponsoredImageLogo | undefined +} + +export type Background = + BraveBackground | + ColorBackground | + CustomBackground | + SponsoredImageBackground + +export interface NewTabState { + backgroundsEnabled: boolean + backgroundsCustomizable: boolean + sponsoredImagesEnabled: boolean + braveBackgrounds: BraveBackground[] + customBackgrounds: string[] + selectedBackgroundType: BackgroundType + selectedBackground: string + currentBackground: Background | null + sponsoredImageBackground: SponsoredImageBackground | null +} + +export function defaultState(): NewTabState { + return { + backgroundsEnabled: true, + backgroundsCustomizable: true, + sponsoredImagesEnabled: true, + braveBackgrounds: [], + customBackgrounds: [], + selectedBackgroundType: 'none', + selectedBackground: '', + currentBackground: null, + sponsoredImageBackground: null + } +} + +export interface NewTabModel { + getState: () => NewTabState + addListener: (listener: (state: NewTabState) => void) => () => void + setBackgroundsEnabled: (enabled: boolean) => void + setSponsoredImagesEnabled: (enabled: boolean) => void + selectBackground: (type: BackgroundType, value: string) => void + showCustomBackgroundChooser: () => Promise + removeCustomBackground: (background: string) => Promise +} + +export function defaultModel(): NewTabModel { + const state = defaultState() + return { + getState() { return state }, + addListener() { return () => {} }, + setBackgroundsEnabled(enabled) {}, + setSponsoredImagesEnabled(enabled) {}, + selectBackground(type, value) {}, + async showCustomBackgroundChooser() { return false }, + async removeCustomBackground(background) {} + } +} diff --git a/browser/resources/brave_new_tab/new_tab_page.html b/browser/resources/brave_new_tab/new_tab_page.html new file mode 100644 index 000000000000..072d0c84ea10 --- /dev/null +++ b/browser/resources/brave_new_tab/new_tab_page.html @@ -0,0 +1,22 @@ + + + + + + New Tab + + + + + + + + + +
+ + diff --git a/browser/resources/brave_new_tab/new_tab_page.tsx b/browser/resources/brave_new_tab/new_tab_page.tsx new file mode 100644 index 000000000000..80ffa724189c --- /dev/null +++ b/browser/resources/brave_new_tab/new_tab_page.tsx @@ -0,0 +1,32 @@ +/* Copyright (c) 2025 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import * as React from 'react' +import { createRoot } from 'react-dom/client' +import { setIconBasePath } from '@brave/leo/react/icon' + +import { LocaleContext } from './components/locale_context' +import { NewTabContext } from './components/new_tab_context' +import { createNewTabModel } from './webui/webui_new_tab_model' +import { createLocale } from './webui/webui_locale' +import { App } from './components/app' + +setIconBasePath('chrome://resources/brave-icons') + +const newTabModel = createNewTabModel() + +Object.assign(self, { + [Symbol.for('ntpInternals')]: { + newTabModel + } +}) + +createRoot(document.getElementById('root')!).render( + + + + + +) diff --git a/browser/resources/brave_new_tab/stories/index.tsx b/browser/resources/brave_new_tab/stories/index.tsx new file mode 100644 index 000000000000..a76598cf659a --- /dev/null +++ b/browser/resources/brave_new_tab/stories/index.tsx @@ -0,0 +1,29 @@ +/* Copyright (c) 2025 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import * as React from 'react' + +import { LocaleContext } from '../components/locale_context' +import { NewTabContext } from '../components/new_tab_context' +import { createNewTabModel } from './sb_new_tab_model' +import { createLocale } from './sb_locale' +import { App } from '../components/app' + +export default { + title: 'New Tab/Next' +} + +export function NTPNext() { + const newTabModel = React.useMemo(() => createNewTabModel(), []) + return ( + + +
+ +
+
+
+ ) +} diff --git a/browser/resources/brave_new_tab/stories/sb_locale.ts b/browser/resources/brave_new_tab/stories/sb_locale.ts new file mode 100644 index 000000000000..1107113a60cb --- /dev/null +++ b/browser/resources/brave_new_tab/stories/sb_locale.ts @@ -0,0 +1,34 @@ +/* Copyright (c) 2025 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import { StringKey } from '../lib/locale_strings' + +const localeStrings: { [K in StringKey]: string } = { + backgroundSettingsTitle: 'Background Image', + braveBackgroundLabel: 'Brave backgrounds', + customBackgroundLabel: 'Use your own', + customBackgroundTitle: 'Use your own', + gradientBackgroundLabel: 'Gradients', + gradientBackgroundTitle: 'Gradients', + photoCreditsText: 'Photo by $1', + randomizeBackgroundLabel: 'Refresh on every new tab', + settingsTitle: 'Customize Dashboard', + showBackgroundsLabel: 'Show background images', + showSponsoredImagesLabel: 'Show Sponsored Images', + solidBackgroundLabel: 'Solid colors', + solidBackgroundTitle: 'Solid colors', + uploadBackgroundLabel: 'Upload from device' +} + +export function createLocale() { + const getString = + (key: string) => String((localeStrings as any)[key] || 'MISSING') + return { + getString, + async getPluralString (key: string, count: number) { + return getString(key).replace('#', String(count)) + } + } +} diff --git a/browser/resources/brave_new_tab/stories/sb_new_tab_model.ts b/browser/resources/brave_new_tab/stories/sb_new_tab_model.ts new file mode 100644 index 000000000000..ed024e654f37 --- /dev/null +++ b/browser/resources/brave_new_tab/stories/sb_new_tab_model.ts @@ -0,0 +1,104 @@ +/* Copyright (c) 2025 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import { createStore } from '../lib/store' +import { getCurrentBackground } from '../models/backgrounds' + +import { + NewTabModel, + SponsoredImageBackground, + defaultModel, + defaultState } from '../models/new_tab_model' + +function delay(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms) + }) +} + +const sampleBackground = + 'https://brave.com/static-assets/images/brave-logo-sans-text.svg' + +const sampleSponsoredImage: SponsoredImageBackground = { + type: 'sponsored', + imageUrl: sampleBackground, + creativeInstanceId: '', + wallpaperId: '', + logo: { + alt: 'Be Brave!', + destinationUrl: 'https://brave.com', + imageUrl: sampleBackground + } +} + +export function createNewTabModel(): NewTabModel { + const store = createStore(defaultState()) + store.update({ + braveBackgrounds: [ + { + type: 'brave', + author: 'John Doe', + imageUrl: sampleBackground, + link: 'https://brave.com' + } + ], + sponsoredImageBackground: sampleSponsoredImage && null + }) + + store.update({ + currentBackground: getCurrentBackground(store.getState()) + }) + + return { + ...defaultModel(), + + getState: store.getState, + addListener: store.addListener, + + setBackgroundsEnabled(enabled) { + store.update({ backgroundsEnabled: enabled }) + store.update({ + currentBackground: getCurrentBackground(store.getState()) + }) + }, + + setSponsoredImagesEnabled(enabled) { + store.update({ sponsoredImagesEnabled: enabled }) + }, + + selectBackground(type, value) { + store.update({ + selectedBackgroundType: type, + selectedBackground: value + }) + store.update({ + currentBackground: getCurrentBackground(store.getState()) + }) + }, + + async showCustomBackgroundChooser() { + delay(200).then(() => { + store.update((state) => ({ + customBackgrounds: [...state.customBackgrounds, sampleBackground], + selectedBackground: sampleBackground, + selectedBackgroundType: 'custom' + })) + + store.update({ + currentBackground: getCurrentBackground(store.getState()) + }) + }) + + return true + }, + + async removeCustomBackground(background) { + store.update((state) => ({ + customBackgrounds: + state.customBackgrounds.filter((elem) => elem !== background) + })) + } + } +} diff --git a/browser/resources/brave_new_tab/tsconfig.json b/browser/resources/brave_new_tab/tsconfig.json new file mode 100644 index 000000000000..3b6366185a85 --- /dev/null +++ b/browser/resources/brave_new_tab/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig", + "include": [ + "**/*.ts", + "**/*.tsx", + "**/*.d.ts" + ] +} diff --git a/browser/resources/brave_new_tab/webui/callback_listeners.ts b/browser/resources/brave_new_tab/webui/callback_listeners.ts new file mode 100644 index 000000000000..c8c5473960f9 --- /dev/null +++ b/browser/resources/brave_new_tab/webui/callback_listeners.ts @@ -0,0 +1,25 @@ +/* Copyright (c) 2025 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +type Router = { + [P in keyof T]: { + addListener: (listener: any) => void + removeListener: (listener: any) => void + } +} + +export function addCallbackListeners( + router: Router, + listeners: Partial +) { + for (const [key, value] of Object.entries(listeners)) { + router[key as keyof T].addListener(value) + } + return () => { + for (const [key, value] of Object.entries(listeners)) { + router[key as keyof T].removeListener(value) + } + } +} diff --git a/browser/resources/brave_new_tab/webui/new_tab_page_proxy.ts b/browser/resources/brave_new_tab/webui/new_tab_page_proxy.ts new file mode 100644 index 000000000000..6c26ff09daac --- /dev/null +++ b/browser/resources/brave_new_tab/webui/new_tab_page_proxy.ts @@ -0,0 +1,35 @@ +/* Copyright (c) 2025 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import * as mojom from 'gen/brave/browser/ui/webui/brave_new_tab/new_tab_page.mojom.m.js' + +import { addCallbackListeners } from './callback_listeners' + +let instance: NewTabPageProxy | null = null + +export class NewTabPageProxy { + callbackRouter: mojom.NewTabPageCallbackRouter + handler: mojom.NewTabPageHandlerRemote + + constructor(callbackRouter: mojom.NewTabPageCallbackRouter, + handler: mojom.NewTabPageHandlerRemote) { + this.callbackRouter = callbackRouter + this.handler = handler + } + + addListeners(listeners: Partial) { + addCallbackListeners(this.callbackRouter, listeners) + } + + static getInstance(): NewTabPageProxy { + if (!instance) { + const callbackRouter = new mojom.NewTabPageCallbackRouter() + const handler = mojom.NewTabPageHandler.getRemote() + handler.setNewTabPage(callbackRouter.$.bindNewPipeAndPassRemote()) + instance = new NewTabPageProxy(callbackRouter, handler) + } + return instance + } +} diff --git a/browser/resources/brave_new_tab/webui/webui_locale.ts b/browser/resources/brave_new_tab/webui/webui_locale.ts new file mode 100644 index 000000000000..a6c8732c2078 --- /dev/null +++ b/browser/resources/brave_new_tab/webui/webui_locale.ts @@ -0,0 +1,18 @@ +/* Copyright (c) 2025 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import { loadTimeData } from 'chrome://resources/js/load_time_data.js' + +export function createLocale() { + return { + getString(key: string) { + return loadTimeData.getString(key) + }, + + async getPluralString(key: string, count: number) { + throw new Error('Not implemented') + } + } +} diff --git a/browser/resources/brave_new_tab/webui/webui_new_tab_model.ts b/browser/resources/brave_new_tab/webui/webui_new_tab_model.ts new file mode 100644 index 000000000000..209dd31c89a1 --- /dev/null +++ b/browser/resources/brave_new_tab/webui/webui_new_tab_model.ts @@ -0,0 +1,153 @@ +/* Copyright (c) 2025 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + + import * as mojom from 'gen/brave/browser/ui/webui/brave_new_tab/new_tab_page.mojom.m.js' + +import { NewTabPageProxy } from './new_tab_page_proxy' +import { NewTabModel, BackgroundType, defaultState } from '../models/new_tab_model' +import { createStore } from '../lib/store' +import { getCurrentBackground } from '../models/backgrounds' +import { debounce } from '$web-common/debounce' + +export function backgroundTypeFromMojo(type: number): BackgroundType { + switch (type) { + case mojom.SelectedBackgroundType.kBrave: return 'brave' + case mojom.SelectedBackgroundType.kCustom: return 'custom' + case mojom.SelectedBackgroundType.kSolid: return 'solid' + case mojom.SelectedBackgroundType.kGradient: return 'gradient' + default: return 'none' + } +} + +export function backgroundTypeToMojo(type: BackgroundType) { + switch (type) { + case 'brave': return mojom.SelectedBackgroundType.kBrave + case 'custom': return mojom.SelectedBackgroundType.kCustom + case 'solid': return mojom.SelectedBackgroundType.kSolid + case 'gradient': return mojom.SelectedBackgroundType.kGradient + case 'none': return mojom.SelectedBackgroundType.kSolid + } +} + +export function createNewTabModel(): NewTabModel { + const newTabProxy = NewTabPageProxy.getInstance() + const { handler } = newTabProxy + const store = createStore(defaultState()) + + function updateCurrentBackground() { + store.update({ + currentBackground: getCurrentBackground(store.getState()) + }) + } + + async function updateBackgroundsEnabled() { + const { enabled } = await handler.getBackgroundsEnabled() + store.update({ backgroundsEnabled: enabled }) + } + + async function updateBackgroundsCustomizable() { + const { customizable } = await handler.getBackgroundsCustomizable() + store.update({ backgroundsCustomizable: customizable }) + } + + async function updateSponsoredImagesEnabled() { + const { enabled } = await handler.getSponsoredImagesEnabled() + store.update({ sponsoredImagesEnabled: enabled }) + } + + async function updateBraveBackgrounds() { + const { backgrounds } = await handler.getBraveBackgrounds() + store.update({ + braveBackgrounds: backgrounds.map((item) => ({ type: 'brave', ...item })) + }) + } + + async function updateSelectedBackground() { + const { background } = await handler.getSelectedBackground() + if (background) { + store.update({ + selectedBackgroundType: backgroundTypeFromMojo(background.type), + selectedBackground: background.value + }) + } else { + store.update({ + selectedBackgroundType: 'none', + selectedBackground: '' + }) + } + } + + async function updateCustomBackgrounds() { + const { backgrounds } = await handler.getCustomBackgrounds() + store.update({ customBackgrounds: backgrounds }) + } + + async function updateSponsoredImageBackground() { + const { background } = await handler.getSponsoredImageBackground() + store.update({ + sponsoredImageBackground: + background ? { type: 'sponsored', ...background } : null + }) + } + + newTabProxy.addListeners({ + onBackgroundPrefsUpdated: debounce(async () => { + await Promise.all([ + updateCustomBackgrounds(), + updateSelectedBackground(), + ]) + updateCurrentBackground() + }, 10) + }) + + async function loadData() { + await Promise.all([ + updateBackgroundsEnabled(), + updateBackgroundsCustomizable(), + updateSponsoredImagesEnabled(), + updateBraveBackgrounds(), + updateCustomBackgrounds(), + updateSelectedBackground(), + updateSponsoredImageBackground() + ]) + + updateCurrentBackground() + } + + loadData() + + return { + getState: store.getState, + + addListener: store.addListener, + + setBackgroundsEnabled(enabled) { + store.update({ backgroundsEnabled: enabled }) + handler.setBackgroundsEnabled(enabled) + }, + + setSponsoredImagesEnabled(enabled) { + store.update({ sponsoredImagesEnabled: enabled }) + handler.setSponsoredImagesEnabled(enabled) + }, + + selectBackground(type, value) { + store.update({ + selectedBackgroundType: type, + selectedBackground: value + }) + handler.selectBackground({ type: backgroundTypeToMojo(type), value }) + }, + + async showCustomBackgroundChooser() { + const { imagesSelected } = await handler.showCustomBackgroundChooser() + return imagesSelected + }, + + async removeCustomBackground(background) { + await handler.removeCustomBackground(background) + } + } +} diff --git a/browser/sources.gni b/browser/sources.gni index d0c67b924f6a..bb386805c5b7 100644 --- a/browser/sources.gni +++ b/browser/sources.gni @@ -288,6 +288,7 @@ if (!is_android) { brave_chrome_browser_deps += [ "//brave/browser/ui/ai_chat", + "//brave/browser/ui/webui/brave_new_tab", "//brave/browser/ui/webui/brave_news_internals", "//components/feed:feature_list", "//components/feed/core/v2:feed_core_v2", @@ -613,3 +614,10 @@ if (is_android) { "//brave/browser/android/preferences", ] } + +# TODO(https://github.com/brave/brave-browser/issues/43310): new_tab_page_ui.cc +# includes some headers that are currently in the //chrome/browser target. +if (!is_android) { + brave_chrome_browser_allow_circular_includes_from += + [ "//brave/browser/ui/webui/brave_new_tab" ] +} diff --git a/browser/ui/config.gni b/browser/ui/config.gni index c640d0a981ea..d46ae5b6ae13 100644 --- a/browser/ui/config.gni +++ b/browser/ui/config.gni @@ -6,6 +6,8 @@ brave_ui_allow_circular_includes_from = [ "//brave/browser/ui/webui/ads_internals" ] if (!is_android) { - brave_ui_allow_circular_includes_from += - [ "//brave/browser/ui/webui/brave_news_internals" ] + brave_ui_allow_circular_includes_from += [ + "//brave/browser/ui/webui/brave_new_tab", + "//brave/browser/ui/webui/brave_news_internals", + ] } diff --git a/browser/ui/webui/brave_new_tab/BUILD.gn b/browser/ui/webui/brave_new_tab/BUILD.gn new file mode 100644 index 000000000000..587784b2fc65 --- /dev/null +++ b/browser/ui/webui/brave_new_tab/BUILD.gn @@ -0,0 +1,51 @@ +# Copyright (c) 2025 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. + +import("//brave/components/ntp_background_images/buildflags/buildflags.gni") +import("//mojo/public/tools/bindings/mojom.gni") + +assert(!is_android) +assert(enable_custom_background) + +mojom("mojom") { + sources = [ "new_tab_page.mojom" ] +} + +source_set("brave_new_tab") { + sources = [ + "background_adapter.cc", + "background_adapter.h", + "custom_image_chooser.cc", + "custom_image_chooser.h", + "new_tab_page_handler.cc", + "new_tab_page_handler.h", + "new_tab_page_ui.cc", + "new_tab_page_ui.h", + "update_observer.cc", + "update_observer.h", + ] + + deps = [ + ":mojom", + "//brave/browser:browser_process", + "//brave/browser/ntp_background", + "//brave/browser/resources/brave_new_tab:generated_resources", + "//brave/components/l10n/common", + "//brave/components/ntp_background_images/browser", + "//brave/components/ntp_background_images/common", + "//brave/components/resources:static_resources", + "//brave/components/resources:strings", + "//chrome/app:generated_resources", + "//chrome/browser:browser_public_dependencies", + "//chrome/browser/profiles:profile", + "//chrome/browser/themes", + "//chrome/browser/ui/webui:webui_util", + "//components/prefs", + "//components/strings:components_strings", + "//ui/base", + "//ui/shell_dialogs", + "//ui/webui", + ] +} diff --git a/browser/ui/webui/brave_new_tab/background_adapter.cc b/browser/ui/webui/brave_new_tab/background_adapter.cc new file mode 100644 index 000000000000..f3753a81c20e --- /dev/null +++ b/browser/ui/webui/brave_new_tab/background_adapter.cc @@ -0,0 +1,328 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/browser/ui/webui/brave_new_tab/background_adapter.h" + +#include + +#include "base/barrier_callback.h" +#include "base/containers/contains.h" +#include "brave/browser/brave_browser_process.h" +#include "brave/browser/ntp_background/custom_background_file_manager.h" +#include "brave/browser/ntp_background/ntp_background_prefs.h" +#include "brave/components/ntp_background_images/browser/ntp_background_images_data.h" +#include "brave/components/ntp_background_images/browser/ntp_background_images_service.h" +#include "brave/components/ntp_background_images/browser/url_constants.h" +#include "brave/components/ntp_background_images/browser/view_counter_service.h" +#include "components/prefs/pref_service.h" + +namespace brave_new_tab { + +namespace { + +std::string GetCustomImageURL(const std::string& image_name) { + return CustomBackgroundFileManager::Converter(image_name).To().spec(); +} + +std::string CustomImageNameFromURL(const std::string& url) { + return CustomBackgroundFileManager::Converter(GURL(url)).To(); +} + +mojom::SponsoredImageBackgroundPtr ReadSponsoredImageData( + const base::Value::Dict& data) { + using ntp_background_images::kAltKey; + using ntp_background_images::kCampaignIdKey; + using ntp_background_images::kCreativeInstanceIDKey; + using ntp_background_images::kDestinationURLKey; + using ntp_background_images::kImageKey; + using ntp_background_images::kIsBackgroundKey; + using ntp_background_images::kLogoKey; + using ntp_background_images::kWallpaperIDKey; + using ntp_background_images::kWallpaperImageURLKey; + + auto is_background = data.FindBool(kIsBackgroundKey); + if (is_background.value_or(false)) { + return nullptr; + } + + auto background = mojom::SponsoredImageBackground::New(); + + if (auto* creative_instance_id = data.FindString(kCreativeInstanceIDKey)) { + background->creative_instance_id = *creative_instance_id; + } + + if (auto* wallpaper_id = data.FindString(kWallpaperIDKey)) { + background->wallpaper_id = *wallpaper_id; + } + + if (auto* campaign_id = data.FindString(kCampaignIdKey)) { + background->campaign_id = *campaign_id; + } + + if (auto* image_url = data.FindString(kWallpaperImageURLKey)) { + background->image_url = *image_url; + } + + if (auto* logo_dict = data.FindDict(kLogoKey)) { + auto logo = mojom::SponsoredImageLogo::New(); + if (auto* alt = logo_dict->FindString(kAltKey)) { + logo->alt = *alt; + } + if (auto* destination_url = logo_dict->FindString(kDestinationURLKey)) { + logo->destination_url = *destination_url; + } + if (auto* image_url = logo_dict->FindString(kImageKey)) { + logo->image_url = *image_url; + } + if (!logo->image_url.empty()) { + background->logo = std::move(logo); + } + } + + return background; +} + +NTPBackgroundPrefs GetBackgroundPrefs(const raw_ref& prefs) { + return NTPBackgroundPrefs(&prefs.get()); +} + +} // namespace + +BackgroundAdapter::BackgroundAdapter( + std::unique_ptr custom_file_manager, + PrefService& pref_service, + ntp_background_images::ViewCounterService* view_counter_service) + : custom_file_manager_(std::move(custom_file_manager)), + pref_service_(pref_service), + view_counter_service_(view_counter_service) { + CHECK(custom_file_manager_); +} + +BackgroundAdapter::~BackgroundAdapter() = default; + +std::vector +BackgroundAdapter::GetBraveBackgrounds() { + auto* service = g_brave_browser_process->ntp_background_images_service(); + if (!service) { + return {}; + } + + auto* image_data = service->GetBackgroundImagesData(); + if (!image_data || !image_data->IsValid()) { + return {}; + } + + std::vector backgrounds; + backgrounds.reserve(image_data->backgrounds.size()); + + for (auto& background : image_data->backgrounds) { + auto value = mojom::BraveBackground::New(); + value->image_url = image_data->url_prefix + + background.image_file.BaseName().AsUTF8Unsafe(); + value->author = background.author; + value->link = background.link; + backgrounds.push_back(std::move(value)); + } + + return backgrounds; +} + +std::vector BackgroundAdapter::GetCustomBackgrounds() { + auto backgrounds = GetBackgroundPrefs(pref_service_).GetCustomImageList(); + for (auto& background : backgrounds) { + background = GetCustomImageURL(background); + } + return backgrounds; +} + +mojom::SelectedBackgroundPtr BackgroundAdapter::GetSelectedBackground() { + auto background = mojom::SelectedBackground::New(); + + auto bg_prefs = GetBackgroundPrefs(pref_service_); + switch (bg_prefs.GetType()) { + case NTPBackgroundPrefs::Type::kBrave: + background->type = mojom::SelectedBackgroundType::kBrave; + break; + case NTPBackgroundPrefs::Type::kCustomImage: + background->type = mojom::SelectedBackgroundType::kCustom; + if (!bg_prefs.ShouldUseRandomValue()) { + background->value = GetCustomImageURL(bg_prefs.GetSelectedValue()); + } + break; + case NTPBackgroundPrefs::Type::kColor: + if (!bg_prefs.ShouldUseRandomValue()) { + background->value = bg_prefs.GetSelectedValue(); + background->type = base::Contains(background->value, "gradient") + ? mojom::SelectedBackgroundType::kGradient + : mojom::SelectedBackgroundType::kSolid; + } else if (bg_prefs.GetSelectedValue() == "gradient") { + background->type = mojom::SelectedBackgroundType::kGradient; + } else { + background->type = mojom::SelectedBackgroundType::kSolid; + } + break; + } + + return background; +} + +mojom::SponsoredImageBackgroundPtr +BackgroundAdapter::GetSponsoredImageBackground() { + if (!view_counter_service_) { + return nullptr; + } + + auto data = view_counter_service_->GetCurrentWallpaperForDisplay(); + if (!data) { + return nullptr; + } + + view_counter_service_->RegisterPageView(); + + auto sponsored_image = ReadSponsoredImageData(*data); + if (sponsored_image) { + view_counter_service_->BrandedWallpaperWillBeDisplayed( + sponsored_image->wallpaper_id, sponsored_image->creative_instance_id, + sponsored_image->campaign_id); + } + + return sponsored_image; +} + +void BackgroundAdapter::SelectBackground( + mojom::SelectedBackgroundPtr background) { + bool random = background->value.empty(); + std::string pref_value = background->value; + + auto bg_prefs = GetBackgroundPrefs(pref_service_); + + switch (background->type) { + case mojom::SelectedBackgroundType::kBrave: + bg_prefs.SetType(NTPBackgroundPrefs::Type::kBrave); + break; + case mojom::SelectedBackgroundType::kSolid: + bg_prefs.SetType(NTPBackgroundPrefs::Type::kColor); + if (random) { + pref_value = "solid"; + } + break; + case mojom::SelectedBackgroundType::kGradient: + bg_prefs.SetType(NTPBackgroundPrefs::Type::kColor); + if (random) { + pref_value = "gradient"; + } + break; + case mojom::SelectedBackgroundType::kCustom: + bg_prefs.SetType(NTPBackgroundPrefs::Type::kCustomImage); + if (!random) { + pref_value = CustomImageNameFromURL(background->value); + } + break; + } + + bg_prefs.SetSelectedValue(pref_value); + bg_prefs.SetShouldUseRandomValue(random); +} + +void BackgroundAdapter::SaveCustomBackgrounds(std::vector paths, + base::OnceClosure callback) { + // Create a repeating callback that will gather up the results of saving the + // custom images to the user's profile. + auto on_image_saved = base::BarrierCallback( + paths.size(), + base::BindOnce(&BackgroundAdapter::OnCustomBackgroundsSaved, + weak_factory_.GetWeakPtr(), std::move(callback))); + + // Since `CustomBackgroundFileManager` will run callbacks with a const ref + // to a base::FilePath, we need another step to copy the path. + auto copy_path = base::BindRepeating( + [](const base::FilePath& path) { return base::FilePath(path); }); + + for (auto& path : paths) { + custom_file_manager_->SaveImage(path, copy_path.Then(on_image_saved)); + } +} + +void BackgroundAdapter::RemoveCustomBackground( + const std::string& background_url, + base::OnceClosure callback) { + auto converter = CustomBackgroundFileManager::Converter( + GURL(background_url), custom_file_manager_.get()); + auto file_path = std::move(converter).To(); + custom_file_manager_->RemoveImage( + file_path, base::BindOnce(&BackgroundAdapter::OnCustomBackgroundRemoved, + weak_factory_.GetWeakPtr(), std::move(callback), + file_path)); +} + +void BackgroundAdapter::OnCustomBackgroundsSaved( + base::OnceClosure callback, + std::vector paths) { + auto bg_prefs = GetBackgroundPrefs(pref_service_); + + constexpr int kMaxCustomImageBackgrounds = 24; + auto can_add_image = [&bg_prefs] { + return bg_prefs.GetCustomImageList().size() < kMaxCustomImageBackgrounds; + }; + + std::string file_name; + + // For each successfully saved image, either add it to the custom image list + // or remove the file from the user's profile. + for (auto& path : paths) { + if (!path.empty()) { + if (can_add_image()) { + file_name = + CustomBackgroundFileManager::Converter(path).To(); + bg_prefs.AddCustomImageToList(file_name); + } else { + custom_file_manager_->RemoveImage(path, base::DoNothing()); + } + } + } + + // Select the last added image file as the current background. + if (!file_name.empty()) { + bg_prefs.SetType(NTPBackgroundPrefs::Type::kCustomImage); + bg_prefs.SetSelectedValue(file_name); + bg_prefs.SetShouldUseRandomValue(false); + } + + std::move(callback).Run(); +} + +void BackgroundAdapter::OnCustomBackgroundRemoved(base::OnceClosure callback, + base::FilePath path, + bool success) { + if (!success) { + std::move(callback).Run(); + return; + } + + auto file_name = + CustomBackgroundFileManager::Converter(path).To(); + + auto bg_prefs = GetBackgroundPrefs(pref_service_); + bg_prefs.RemoveCustomImageFromList(file_name); + + // If we are removing the currently selected background, either select the + // first remaining custom background, or, if there are none left, then select + // a default background. + if (bg_prefs.GetType() == NTPBackgroundPrefs::Type::kCustomImage && + bg_prefs.GetSelectedValue() == file_name) { + auto custom_images = bg_prefs.GetCustomImageList(); + if (custom_images.empty()) { + bg_prefs.SetType(NTPBackgroundPrefs::Type::kBrave); + bg_prefs.SetSelectedValue(""); + bg_prefs.SetShouldUseRandomValue(true); + } else { + bg_prefs.SetSelectedValue(custom_images.front()); + } + } + + std::move(callback).Run(); +} + +} // namespace brave_new_tab diff --git a/browser/ui/webui/brave_new_tab/background_adapter.h b/browser/ui/webui/brave_new_tab/background_adapter.h new file mode 100644 index 000000000000..b6beb32a51a1 --- /dev/null +++ b/browser/ui/webui/brave_new_tab/background_adapter.h @@ -0,0 +1,71 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_BROWSER_UI_WEBUI_BRAVE_NEW_TAB_BACKGROUND_ADAPTER_H_ +#define BRAVE_BROWSER_UI_WEBUI_BRAVE_NEW_TAB_BACKGROUND_ADAPTER_H_ + +#include +#include +#include + +#include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" +#include "brave/browser/ui/webui/brave_new_tab/new_tab_page.mojom.h" + +class CustomBackgroundFileManager; +class PrefService; + +namespace ntp_background_images { +class ViewCounterService; +} + +namespace brave_new_tab { + +// Provides access to background-related APIs for usage by the new tab page. +class BackgroundAdapter { + public: + BackgroundAdapter( + std::unique_ptr custom_file_manager, + PrefService& pref_service, + ntp_background_images::ViewCounterService* view_counter_service); + + ~BackgroundAdapter(); + + BackgroundAdapter(const BackgroundAdapter&) = delete; + BackgroundAdapter& operator=(const BackgroundAdapter&) = delete; + + std::vector GetBraveBackgrounds(); + + std::vector GetCustomBackgrounds(); + + mojom::SelectedBackgroundPtr GetSelectedBackground(); + + mojom::SponsoredImageBackgroundPtr GetSponsoredImageBackground(); + + void SelectBackground(mojom::SelectedBackgroundPtr background); + + void SaveCustomBackgrounds(std::vector paths, + base::OnceClosure callback); + + void RemoveCustomBackground(const std::string& background_url, + base::OnceClosure callback); + + private: + void OnCustomBackgroundsSaved(base::OnceClosure callback, + std::vector paths); + + void OnCustomBackgroundRemoved(base::OnceClosure callback, + base::FilePath path, + bool success); + + std::unique_ptr custom_file_manager_; + raw_ref pref_service_; + raw_ptr view_counter_service_; + base::WeakPtrFactory weak_factory_{this}; +}; + +} // namespace brave_new_tab + +#endif // BRAVE_BROWSER_UI_WEBUI_BRAVE_NEW_TAB_BACKGROUND_ADAPTER_H_ diff --git a/browser/ui/webui/brave_new_tab/custom_image_chooser.cc b/browser/ui/webui/brave_new_tab/custom_image_chooser.cc new file mode 100644 index 000000000000..a6ddbe43e849 --- /dev/null +++ b/browser/ui/webui/brave_new_tab/custom_image_chooser.cc @@ -0,0 +1,86 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/browser/ui/webui/brave_new_tab/custom_image_chooser.h" + +#include + +#include "brave/components/l10n/common/localization_util.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/ui/chrome_select_file_policy.h" +#include "chrome/grit/generated_resources.h" +#include "content/public/browser/web_contents.h" +#include "ui/shell_dialogs/selected_file_info.h" + +namespace brave_new_tab { + +CustomImageChooser::CustomImageChooser(content::WebContents& web_contents) + : web_contents_(web_contents), + profile_(*Profile::FromBrowserContext(web_contents.GetBrowserContext())) { +} + +CustomImageChooser::~CustomImageChooser() = default; + +void CustomImageChooser::ShowDialog(ShowDialogCallback callback) { + if (callback_) { + std::move(callback_).Run({}); + } + + callback_ = std::move(callback); + + if (dialog_) { + return; + } + + dialog_ = ui::SelectFileDialog::Create( + this, std::make_unique(&web_contents_.get())); + + ui::SelectFileDialog::FileTypeInfo file_types; + file_types.allowed_paths = ui::SelectFileDialog::FileTypeInfo::NATIVE_PATH; + file_types.extensions.push_back( + {{FILE_PATH_LITERAL("jpg"), FILE_PATH_LITERAL("jpeg"), + FILE_PATH_LITERAL("png"), FILE_PATH_LITERAL("gif")}}); + file_types.extension_description_overrides.push_back( + brave_l10n::GetLocalizedResourceUTF16String(IDS_UPLOAD_IMAGE_FORMAT)); + + dialog_->SelectFile(ui::SelectFileDialog::SELECT_OPEN_MULTI_FILE, + std::u16string(), profile_->last_selected_directory(), + &file_types, 0, base::FilePath::StringType(), + web_contents_->GetTopLevelNativeWindow(), nullptr); +} + +void CustomImageChooser::FileSelected(const ui::SelectedFileInfo& file, + int index) { + dialog_ = nullptr; + profile_->set_last_selected_directory(file.path().DirName()); + if (callback_) { + std::move(callback_).Run({file.path()}); + } +} + +void CustomImageChooser::MultiFilesSelected( + const std::vector& files) { + dialog_ = nullptr; + if (!files.empty()) { + profile_->set_last_selected_directory(files.back().path().DirName()); + } + std::vector paths; + paths.reserve(files.size()); + for (auto& file : files) { + paths.push_back(file.path()); + } + if (callback_) { + std::move(callback_).Run(std::move(paths)); + } +} + +void CustomImageChooser::FileSelectionCanceled() { + dialog_ = nullptr; + if (callback_) { + std::move(callback_).Run({}); + } +} + +} // namespace brave_new_tab diff --git a/browser/ui/webui/brave_new_tab/custom_image_chooser.h b/browser/ui/webui/brave_new_tab/custom_image_chooser.h new file mode 100644 index 000000000000..606a5c73b12d --- /dev/null +++ b/browser/ui/webui/brave_new_tab/custom_image_chooser.h @@ -0,0 +1,55 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_BROWSER_UI_WEBUI_BRAVE_NEW_TAB_CUSTOM_IMAGE_CHOOSER_H_ +#define BRAVE_BROWSER_UI_WEBUI_BRAVE_NEW_TAB_CUSTOM_IMAGE_CHOOSER_H_ + +#include +#include + +#include "base/files/file_path.h" +#include "base/functional/callback.h" +#include "base/memory/raw_ref.h" +#include "ui/shell_dialogs/select_file_dialog.h" + +class Profile; + +namespace content { +class WebContents; +} + +namespace brave_new_tab { + +// Displays a file chooser dialog for use on the New Tab Page, allowing the user +// to select background images from their device. +class CustomImageChooser : public ui::SelectFileDialog::Listener { + public: + explicit CustomImageChooser(content::WebContents& web_contents); + ~CustomImageChooser() override; + + CustomImageChooser(const CustomImageChooser&) = delete; + CustomImageChooser& operator=(const CustomImageChooser&) = delete; + + using ShowDialogCallback = + base::OnceCallback)>; + + void ShowDialog(ShowDialogCallback callback); + + // ui::SelectFileDialog::Listener: + void FileSelected(const ui::SelectedFileInfo& file, int index) override; + void MultiFilesSelected( + const std::vector& files) override; + void FileSelectionCanceled() override; + + private: + raw_ref web_contents_; + raw_ref profile_; + scoped_refptr dialog_; + ShowDialogCallback callback_; +}; + +} // namespace brave_new_tab + +#endif // BRAVE_BROWSER_UI_WEBUI_BRAVE_NEW_TAB_CUSTOM_IMAGE_CHOOSER_H_ diff --git a/browser/ui/webui/brave_new_tab/new_tab_page.mojom b/browser/ui/webui/brave_new_tab/new_tab_page.mojom new file mode 100644 index 000000000000..4f1bd5a6a51a --- /dev/null +++ b/browser/ui/webui/brave_new_tab/new_tab_page.mojom @@ -0,0 +1,94 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +module brave_new_tab.mojom; + +struct BraveBackground { + string author; + string image_url; + string link; +}; + +enum SelectedBackgroundType { + kBrave, + kCustom, + kSolid, + kGradient, +}; + +struct SelectedBackground { + SelectedBackgroundType type; + string value; +}; + +struct SponsoredImageLogo { + string alt; + string destination_url; + string image_url; +}; + +struct SponsoredImageBackground { + string wallpaper_id; + string creative_instance_id; + string campaign_id; + string image_url; + SponsoredImageLogo? logo; +}; + +// WebUI-side handler for notifications from the browser. +interface NewTabPage { + + // Called when a background-related profile preference has been updated. + OnBackgroundPrefsUpdated(); + +}; + +// Browser-side handler for requests from the WebUI page. +interface NewTabPageHandler { + + // Sets the NewTabPage remote interface that will receive notifications from + // the browser. + SetNewTabPage(pending_remote page); + + // Gets or sets whether the user has enabled background images or colors on + // the new tab page. + GetBackgroundsEnabled() => (bool enabled); + SetBackgroundsEnabled(bool enabled) => (); + + // Returns a value indicating whether user-selectable backgrounds are + // supported for the current profile. + GetBackgroundsCustomizable() => (bool customizable); + + // Gets or sets whether the user has enabled sponsored background images. + GetSponsoredImagesEnabled() => (bool enabled); + SetSponsoredImagesEnabled(bool enabled) => (); + + // Returns the current collection of Brave background images. + GetBraveBackgrounds() => (array backgrounds); + + // Returns the list of custom background images supplied by the user. + GetCustomBackgrounds() => (array backgrounds); + + // Returns the user-selected or default background. + GetSelectedBackground() => (SelectedBackground? background); + + // Returns sponsored image background info, if a sponsored image should be + // displayed to the user. + GetSponsoredImageBackground() => (SponsoredImageBackground? background); + + // Saves the provided background as the user's selected background. + SelectBackground(SelectedBackground background) => (); + + // Displays a file select dialog for selecting custom background images and + // returns a value indicating whether any image files were selected. If + // images were selected, they are added to the user's profile in the + // background. + ShowCustomBackgroundChooser() => (bool images_selected); + + // Removes the specified custom image background from the list of available + // backgrounds. + RemoveCustomBackground(string background_url) => (); + +}; diff --git a/browser/ui/webui/brave_new_tab/new_tab_page_handler.cc b/browser/ui/webui/brave_new_tab/new_tab_page_handler.cc new file mode 100644 index 000000000000..30c38976ad8c --- /dev/null +++ b/browser/ui/webui/brave_new_tab/new_tab_page_handler.cc @@ -0,0 +1,148 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/browser/ui/webui/brave_new_tab/new_tab_page_handler.h" + +#include + +#include "brave/browser/ui/webui/brave_new_tab/background_adapter.h" +#include "brave/browser/ui/webui/brave_new_tab/custom_image_chooser.h" +#include "brave/components/ntp_background_images/common/pref_names.h" +#include "chrome/browser/themes/theme_syncable_service.h" +#include "components/prefs/pref_service.h" + +namespace brave_new_tab { + +NewTabPageHandler::NewTabPageHandler( + mojo::PendingReceiver receiver, + std::unique_ptr custom_image_chooser, + std::unique_ptr background_adapter, + PrefService& pref_service) + : receiver_(this, std::move(receiver)), + update_observer_(pref_service), + custom_image_chooser_(std::move(custom_image_chooser)), + background_adapter_(std::move(background_adapter)), + pref_service_(pref_service) { + CHECK(custom_image_chooser_); + CHECK(background_adapter_); + + update_observer_.SetCallback(base::BindRepeating(&NewTabPageHandler::OnUpdate, + weak_factory_.GetWeakPtr())); +} + +NewTabPageHandler::~NewTabPageHandler() = default; + +void NewTabPageHandler::SetNewTabPage( + mojo::PendingRemote page) { + page_.reset(); + page_.Bind(std::move(page)); +} + +void NewTabPageHandler::GetBackgroundsEnabled( + GetBackgroundsEnabledCallback callback) { + bool backgrounds_enabled = pref_service_->GetBoolean( + ntp_background_images::prefs::kNewTabPageShowBackgroundImage); + std::move(callback).Run(backgrounds_enabled); +} + +void NewTabPageHandler::SetBackgroundsEnabled( + bool enabled, + SetBackgroundsEnabledCallback callback) { + pref_service_->SetBoolean( + ntp_background_images::prefs::kNewTabPageShowBackgroundImage, enabled); + std::move(callback).Run(); +} + +void NewTabPageHandler::GetBackgroundsCustomizable( + GetBackgroundsCustomizableCallback callback) { + std::move(callback).Run( + !pref_service_->IsManagedPreference(GetThemePrefNameInMigration( + ThemePrefInMigration::kNtpCustomBackgroundDict))); +} + +void NewTabPageHandler::GetSponsoredImagesEnabled( + GetSponsoredImagesEnabledCallback callback) { + bool sponsored_images_enabled = pref_service_->GetBoolean( + ntp_background_images::prefs:: + kNewTabPageShowSponsoredImagesBackgroundImage); + std::move(callback).Run(sponsored_images_enabled); +} + +void NewTabPageHandler::SetSponsoredImagesEnabled( + bool enabled, + SetSponsoredImagesEnabledCallback callback) { + pref_service_->SetBoolean(ntp_background_images::prefs:: + kNewTabPageShowSponsoredImagesBackgroundImage, + enabled); + std::move(callback).Run(); +} + +void NewTabPageHandler::GetBraveBackgrounds( + GetBraveBackgroundsCallback callback) { + std::move(callback).Run(background_adapter_->GetBraveBackgrounds()); +} + +void NewTabPageHandler::GetCustomBackgrounds( + GetCustomBackgroundsCallback callback) { + std::move(callback).Run(background_adapter_->GetCustomBackgrounds()); +} + +void NewTabPageHandler::GetSelectedBackground( + GetSelectedBackgroundCallback callback) { + std::move(callback).Run(background_adapter_->GetSelectedBackground()); +} + +void NewTabPageHandler::GetSponsoredImageBackground( + GetSponsoredImageBackgroundCallback callback) { + std::move(callback).Run(background_adapter_->GetSponsoredImageBackground()); +} + +void NewTabPageHandler::SelectBackground( + mojom::SelectedBackgroundPtr background, + SelectBackgroundCallback callback) { + background_adapter_->SelectBackground(std::move(background)); + std::move(callback).Run(); +} + +void NewTabPageHandler::ShowCustomBackgroundChooser( + ShowCustomBackgroundChooserCallback callback) { + custom_image_chooser_->ShowDialog( + base::BindOnce(&NewTabPageHandler::OnCustomBackgroundsSelected, + weak_factory_.GetWeakPtr(), std::move(callback))); +} + +void NewTabPageHandler::RemoveCustomBackground( + const std::string& background_url, + RemoveCustomBackgroundCallback callback) { + background_adapter_->RemoveCustomBackground(background_url, + std::move(callback)); +} + +void NewTabPageHandler::OnCustomBackgroundsSelected( + ShowCustomBackgroundChooserCallback callback, + std::vector paths) { + // Before continuing, notify the caller of whether backgrounds were selected. + // This allows the front-end to display a loading indicator while the save + // operation is in progress. + std::move(callback).Run(!paths.empty()); + + if (!paths.empty()) { + background_adapter_->SaveCustomBackgrounds(std::move(paths), + base::DoNothing()); + } +} + +void NewTabPageHandler::OnUpdate(UpdateObserver::Source update_source) { + if (!page_.is_bound()) { + return; + } + switch (update_source) { + case UpdateObserver::Source::kBackgroundPrefs: + page_->OnBackgroundPrefsUpdated(); + break; + } +} + +} // namespace brave_new_tab diff --git a/browser/ui/webui/brave_new_tab/new_tab_page_handler.h b/browser/ui/webui/brave_new_tab/new_tab_page_handler.h new file mode 100644 index 000000000000..835bd37514b2 --- /dev/null +++ b/browser/ui/webui/brave_new_tab/new_tab_page_handler.h @@ -0,0 +1,79 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_BROWSER_UI_WEBUI_BRAVE_NEW_TAB_NEW_TAB_PAGE_HANDLER_H_ +#define BRAVE_BROWSER_UI_WEBUI_BRAVE_NEW_TAB_NEW_TAB_PAGE_HANDLER_H_ + +#include +#include +#include + +#include "base/memory/raw_ref.h" +#include "base/memory/weak_ptr.h" +#include "brave/browser/ui/webui/brave_new_tab/new_tab_page.mojom.h" +#include "brave/browser/ui/webui/brave_new_tab/update_observer.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "mojo/public/cpp/bindings/pending_remote.h" +#include "mojo/public/cpp/bindings/receiver.h" +#include "mojo/public/cpp/bindings/remote.h" + +class PrefService; + +namespace brave_new_tab { + +class BackgroundAdapter; +class CustomImageChooser; + +class NewTabPageHandler : public mojom::NewTabPageHandler { + public: + NewTabPageHandler(mojo::PendingReceiver receiver, + std::unique_ptr custom_image_chooser, + std::unique_ptr background_adapter, + PrefService& pref_service); + + ~NewTabPageHandler() override; + + // mojom::NewTabPageHandler: + void SetNewTabPage(mojo::PendingRemote page) override; + void GetBackgroundsEnabled(GetBackgroundsEnabledCallback callback) override; + void SetBackgroundsEnabled(bool enabled, + SetBackgroundsEnabledCallback callback) override; + void GetBackgroundsCustomizable( + GetBackgroundsCustomizableCallback callback) override; + void GetSponsoredImagesEnabled( + GetSponsoredImagesEnabledCallback callback) override; + void SetSponsoredImagesEnabled( + bool enabled, + SetSponsoredImagesEnabledCallback callback) override; + void GetBraveBackgrounds(GetBraveBackgroundsCallback callback) override; + void GetCustomBackgrounds(GetCustomBackgroundsCallback callback) override; + void GetSelectedBackground(GetSelectedBackgroundCallback callback) override; + void GetSponsoredImageBackground( + GetSponsoredImageBackgroundCallback callback) override; + void SelectBackground(mojom::SelectedBackgroundPtr background, + SelectBackgroundCallback callback) override; + void ShowCustomBackgroundChooser( + ShowCustomBackgroundChooserCallback callback) override; + void RemoveCustomBackground(const std::string& background_url, + RemoveCustomBackgroundCallback callback) override; + + private: + void OnCustomBackgroundsSelected(ShowCustomBackgroundChooserCallback callback, + std::vector paths); + + void OnUpdate(UpdateObserver::Source update_source); + + mojo::Receiver receiver_; + mojo::Remote page_; + UpdateObserver update_observer_; + std::unique_ptr custom_image_chooser_; + std::unique_ptr background_adapter_; + raw_ref pref_service_; + base::WeakPtrFactory weak_factory_{this}; +}; + +} // namespace brave_new_tab + +#endif // BRAVE_BROWSER_UI_WEBUI_BRAVE_NEW_TAB_NEW_TAB_PAGE_HANDLER_H_ diff --git a/browser/ui/webui/brave_new_tab/new_tab_page_ui.cc b/browser/ui/webui/brave_new_tab/new_tab_page_ui.cc new file mode 100644 index 000000000000..5f259caff9b6 --- /dev/null +++ b/browser/ui/webui/brave_new_tab/new_tab_page_ui.cc @@ -0,0 +1,118 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/browser/ui/webui/brave_new_tab/new_tab_page_ui.h" + +#include + +#include "brave/browser/new_tab/new_tab_shows_options.h" +#include "brave/browser/ntp_background/brave_ntp_custom_background_service_factory.h" +#include "brave/browser/ntp_background/custom_background_file_manager.h" +#include "brave/browser/ntp_background/view_counter_service_factory.h" +#include "brave/browser/resources/brave_new_tab/grit/brave_new_tab_generated_map.h" +#include "brave/browser/ui/webui/brave_new_tab/background_adapter.h" +#include "brave/browser/ui/webui/brave_new_tab/custom_image_chooser.h" +#include "brave/browser/ui/webui/brave_new_tab/new_tab_page_handler.h" +#include "brave/browser/ui/webui/brave_webui_source.h" +#include "brave/components/l10n/common/localization_util.h" +#include "brave/components/ntp_background_images/browser/ntp_custom_images_source.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/ui/webui/webui_util.h" +#include "components/grit/brave_components_resources.h" +#include "components/grit/brave_components_strings.h" +#include "components/prefs/pref_service.h" +#include "components/strings/grit/components_strings.h" +#include "content/public/browser/web_ui.h" +#include "content/public/browser/web_ui_data_source.h" +#include "ui/base/webui/web_ui_util.h" + +namespace brave_new_tab { + +namespace { + +static constexpr webui::LocalizedString kStrings[] = { + {"backgroundSettingsTitle", IDS_NEW_TAB_BACKGROUND_SETTINGS_TITLE}, + {"braveBackgroundLabel", IDS_NEW_TAB_BRAVE_BACKGROUND_LABEL}, + {"customBackgroundLabel", IDS_NEW_TAB_CUSTOM_BACKGROUND_LABEL}, + {"customBackgroundTitle", IDS_NEW_TAB_CUSTOM_BACKGROUND_LABEL}, + {"gradientBackgroundLabel", IDS_NEW_TAB_GRADIENT_BACKGROUND_LABEL}, + {"gradientBackgroundTitle", IDS_NEW_TAB_GRADIENT_BACKGROUND_LABEL}, + {"photoCreditsText", IDS_NEW_TAB_PHOTO_CREDITS_TEXT}, + {"randomizeBackgroundLabel", IDS_NEW_TAB_RANDOMIZE_BACKGROUND_LABEL}, + {"settingsTitle", IDS_NEW_TAB_SETTINGS_TITLE}, + {"showBackgroundsLabel", IDS_NEW_TAB_SHOW_BACKGROUNDS_LABEL}, + {"showSponsoredImagesLabel", IDS_NEW_TAB_SHOW_SPONSORED_IMAGES_LABEL}, + {"solidBackgroundLabel", IDS_NEW_TAB_SOLID_BACKGROUND_LABEL}, + {"solidBackgroundTitle", IDS_NEW_TAB_SOLID_BACKGROUND_LABEL}, + {"uploadBackgroundLabel", IDS_NEW_TAB_UPLOAD_BACKGROUND_LABEL}, +}; + +// Adds support for displaying images stored in the custom background image +// folder. +void AddCustomImageDataSource(Profile* profile) { + auto* custom_background_service = + BraveNTPCustomBackgroundServiceFactory::GetForContext(profile); + if (!custom_background_service) { + return; + } + auto source = std::make_unique( + custom_background_service); + content::URLDataSource::Add(profile, std::move(source)); +} + +} // namespace + +NewTabPageUI::NewTabPageUI(content::WebUI* web_ui) + : ui::MojoWebUIController(web_ui) { + auto* profile = Profile::FromWebUI(web_ui); + + auto* source = content::WebUIDataSource::CreateAndAdd( + profile, chrome::kChromeUINewTabHost); + + if (brave::ShouldNewTabShowBlankpage(profile)) { + source->SetDefaultResource(IDR_BRAVE_BLANK_NEW_TAB_HTML); + } else { + webui::SetupWebUIDataSource(source, kBraveNewTabGenerated, + IDR_BRAVE_NEW_TAB_PAGE_HTML); + } + + source->OverrideContentSecurityPolicy( + network::mojom::CSPDirectiveName::ImgSrc, + "img-src chrome://resources chrome://theme chrome://background-wallpaper " + "chrome://custom-wallpaper chrome://branded-wallpaper chrome://favicon2 " + "blob: data: 'self';"); + + AddBackgroundColorToSource(source, web_ui->GetWebContents()); + AddCustomImageDataSource(profile); + + web_ui->OverrideTitle( + brave_l10n::GetLocalizedResourceUTF16String(IDS_NEW_TAB_TITLE)); + + source->AddLocalizedStrings(kStrings); +} + +NewTabPageUI::~NewTabPageUI() = default; + +void NewTabPageUI::BindInterface( + mojo::PendingReceiver pending_receiver) { + auto* profile = Profile::FromWebUI(web_ui()); + + auto* prefs = profile->GetPrefs(); + + auto image_chooser = + std::make_unique(*web_ui()->GetWebContents()); + + auto background_adapter = std::make_unique( + std::make_unique(profile), *prefs, + ntp_background_images::ViewCounterServiceFactory::GetForProfile(profile)); + + page_handler_ = std::make_unique( + std::move(pending_receiver), std::move(image_chooser), + std::move(background_adapter), *prefs); +} + +WEB_UI_CONTROLLER_TYPE_IMPL(NewTabPageUI) + +} // namespace brave_new_tab diff --git a/browser/ui/webui/brave_new_tab/new_tab_page_ui.h b/browser/ui/webui/brave_new_tab/new_tab_page_ui.h new file mode 100644 index 000000000000..b1bf14f26ad1 --- /dev/null +++ b/browser/ui/webui/brave_new_tab/new_tab_page_ui.h @@ -0,0 +1,39 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_BROWSER_UI_WEBUI_BRAVE_NEW_TAB_NEW_TAB_PAGE_UI_H_ +#define BRAVE_BROWSER_UI_WEBUI_BRAVE_NEW_TAB_NEW_TAB_PAGE_UI_H_ + +#include + +#include "brave/browser/ui/webui/brave_new_tab/new_tab_page.mojom.h" +#include "chrome/common/webui_url_constants.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "ui/webui/mojo_web_ui_controller.h" + +namespace content { +class WebUI; +} + +namespace brave_new_tab { + +// The Web UI controller for the Brave new tab page. +class NewTabPageUI : public ui::MojoWebUIController { + public: + explicit NewTabPageUI(content::WebUI* web_ui); + ~NewTabPageUI() override; + + void BindInterface( + mojo::PendingReceiver pending_receiver); + + private: + std::unique_ptr page_handler_; + + WEB_UI_CONTROLLER_TYPE_DECL(); +}; + +} // namespace brave_new_tab + +#endif // BRAVE_BROWSER_UI_WEBUI_BRAVE_NEW_TAB_NEW_TAB_PAGE_UI_H_ diff --git a/browser/ui/webui/brave_new_tab/update_observer.cc b/browser/ui/webui/brave_new_tab/update_observer.cc new file mode 100644 index 000000000000..0102424a42e2 --- /dev/null +++ b/browser/ui/webui/brave_new_tab/update_observer.cc @@ -0,0 +1,52 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/browser/ui/webui/brave_new_tab/update_observer.h" + +#include + +#include "brave/browser/ntp_background/ntp_background_prefs.h" +#include "brave/components/ntp_background_images/common/pref_names.h" + +namespace brave_new_tab { + +UpdateObserver::UpdateObserver(PrefService& pref_service) { + pref_change_registrar_.Init(&pref_service); + AddPrefListener(ntp_background_images::prefs::kNewTabPageShowBackgroundImage, + Source::kBackgroundPrefs); + AddPrefListener(ntp_background_images::prefs:: + kNewTabPageShowSponsoredImagesBackgroundImage, + Source::kBackgroundPrefs); + AddPrefListener(NTPBackgroundPrefs::kPrefName, Source::kBackgroundPrefs); + AddPrefListener(NTPBackgroundPrefs::kCustomImageListPrefName, + Source::kBackgroundPrefs); +} + +UpdateObserver::~UpdateObserver() = default; + +void UpdateObserver::SetCallback( + base::RepeatingCallback callback) { + callback_ = std::move(callback); +} + +void UpdateObserver::OnUpdate(Source update_source) { + if (callback_) { + callback_.Run(update_source); + } +} + +void UpdateObserver::OnPrefChanged(Source update_kind, + const std::string& path) { + OnUpdate(update_kind); +} + +void UpdateObserver::AddPrefListener(const std::string& path, + Source update_source) { + pref_change_registrar_.Add( + path, base::BindRepeating(&UpdateObserver::OnPrefChanged, + weak_factory_.GetWeakPtr(), update_source)); +} + +} // namespace brave_new_tab diff --git a/browser/ui/webui/brave_new_tab/update_observer.h b/browser/ui/webui/brave_new_tab/update_observer.h new file mode 100644 index 000000000000..58ddcddf55fd --- /dev/null +++ b/browser/ui/webui/brave_new_tab/update_observer.h @@ -0,0 +1,45 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_BROWSER_UI_WEBUI_BRAVE_NEW_TAB_UPDATE_OBSERVER_H_ +#define BRAVE_BROWSER_UI_WEBUI_BRAVE_NEW_TAB_UPDATE_OBSERVER_H_ + +#include + +#include "base/functional/callback.h" +#include "base/memory/weak_ptr.h" +#include "components/prefs/pref_change_registrar.h" + +class PrefService; + +namespace brave_new_tab { + +// Listens for changes to profile and system state that must be reflected on the +// new tab page. +class UpdateObserver { + public: + enum class Source { kBackgroundPrefs }; + + explicit UpdateObserver(PrefService& pref_service); + ~UpdateObserver(); + + UpdateObserver(const UpdateObserver&) = delete; + UpdateObserver& operator=(const UpdateObserver&) = delete; + + void SetCallback(base::RepeatingCallback callback); + + private: + void OnUpdate(Source update_source); + void OnPrefChanged(Source update_source, const std::string& path); + void AddPrefListener(const std::string& path, Source update_source); + + PrefChangeRegistrar pref_change_registrar_; + base::RepeatingCallback callback_; + base::WeakPtrFactory weak_factory_{this}; +}; + +} // namespace brave_new_tab + +#endif // BRAVE_BROWSER_UI_WEBUI_BRAVE_NEW_TAB_UPDATE_OBSERVER_H_ diff --git a/browser/ui/webui/brave_web_ui_controller_factory.cc b/browser/ui/webui/brave_web_ui_controller_factory.cc index 46c16d5feb36..bfc76c7672df 100644 --- a/browser/ui/webui/brave_web_ui_controller_factory.cc +++ b/browser/ui/webui/brave_web_ui_controller_factory.cc @@ -12,6 +12,7 @@ #include "base/memory/ptr_util.h" #include "base/no_destructor.h" #include "brave/browser/brave_ads/ads_service_factory.h" +#include "brave/browser/brave_browser_features.h" #include "brave/browser/brave_news/brave_news_controller_factory.h" #include "brave/browser/brave_rewards/rewards_util.h" #include "brave/browser/ethereum_remote_client/buildflags/buildflags.h" @@ -42,6 +43,7 @@ #if !BUILDFLAG(IS_ANDROID) #include "brave/browser/brave_wallet/brave_wallet_context_utils.h" +#include "brave/browser/ui/webui/brave_new_tab/new_tab_page_ui.h" #include "brave/browser/ui/webui/brave_news_internals/brave_news_internals_ui.h" #include "brave/browser/ui/webui/brave_wallet/wallet_page_ui.h" #include "brave/browser/ui/webui/new_tab_page/brave_new_tab_ui.h" @@ -151,6 +153,9 @@ WebUIController* NewWebUI(WebUI* web_ui, const GURL& url) { // WebUIConfig. Currently, we can't add both BravePrivateNewTabUI and // BraveNewTabUI configs in RegisterChromeWebUIConfigs because they use the // same origin (content::kChromeUIScheme + chrome::kChromeUINewTabHost). + if (base::FeatureList::IsEnabled(features::kUseUpdatedNTP)) { + return new brave_new_tab::NewTabPageUI(web_ui); + } return new BraveNewTabUI(web_ui, url.host()); #endif // !BUILDFLAG(IS_ANDROID) #if BUILDFLAG(ENABLE_TOR) diff --git a/browser/ui/webui/new_tab_page/DEPS b/browser/ui/webui/new_tab_page/DEPS index d72c8fe63bfa..afbfb77f9379 100644 --- a/browser/ui/webui/new_tab_page/DEPS +++ b/browser/ui/webui/new_tab_page/DEPS @@ -1,6 +1,6 @@ include_rules = [ "+brave/components/brave_ads/core/browser/service", - "+brave/components/brave_new_tab/resources", + "+brave/components/brave_new_tab_ui/resources", "+brave/components/brave_perf_predictor/common", "+brave/components/ntp_background_images/common", "+brave/components/time_period_storage", diff --git a/browser/ui/webui/new_tab_page/brave_new_tab_ui.cc b/browser/ui/webui/new_tab_page/brave_new_tab_ui.cc index f33115a8f2ed..9e03c6aebd87 100644 --- a/browser/ui/webui/new_tab_page/brave_new_tab_ui.cc +++ b/browser/ui/webui/new_tab_page/brave_new_tab_ui.cc @@ -20,7 +20,7 @@ #include "brave/browser/ui/webui/new_tab_page/brave_new_tab_message_handler.h" #include "brave/browser/ui/webui/new_tab_page/brave_new_tab_page_handler.h" #include "brave/browser/ui/webui/new_tab_page/top_sites_message_handler.h" -#include "brave/components/brave_new_tab/resources/grit/brave_new_tab_generated_map.h" +#include "brave/components/brave_new_tab_ui/grit/brave_new_tab_ui_generated_map.h" #include "brave/components/brave_news/browser/brave_news_controller.h" #include "brave/components/brave_news/common/features.h" #include "brave/components/l10n/common/localization_util.h" @@ -74,7 +74,7 @@ BraveNewTabUI::BraveNewTabUI(content::WebUI* web_ui, const std::string& name) // Non blank NTP. content::WebUIDataSource* source = CreateAndAddWebUIDataSource( - web_ui, name, kBraveNewTabGenerated, kBraveNewTabGeneratedSize, + web_ui, name, kBraveNewTabUiGenerated, kBraveNewTabUiGeneratedSize, IDR_BRAVE_NEW_TAB_HTML); AddBackgroundColorToSource(source, web_contents); diff --git a/components/brave_new_tab_ui/BUILD.gn b/components/brave_new_tab_ui/BUILD.gn index d6a361bcba58..64a332ee4d93 100644 --- a/components/brave_new_tab_ui/BUILD.gn +++ b/components/brave_new_tab_ui/BUILD.gn @@ -9,7 +9,7 @@ import("//mojo/public/tools/bindings/mojom.gni") transpile_web_ui("brave_new_tab_ui") { entry_points = [ [ - "brave_new_tab", + "brave_new_tab_ui", rebase_path("brave_new_tab.tsx"), ] ] public_deps = [ @@ -23,12 +23,12 @@ transpile_web_ui("brave_new_tab_ui") { "//ui/webui/resources/cr_components/searchbox:mojo_bindings_js", ] } - resource_name = "brave_new_tab" + resource_name = "brave_new_tab_ui" } pack_web_resources("generated_resources") { - resource_name = "brave_new_tab" - output_dir = "$root_gen_dir/brave/components/brave_new_tab/resources" + resource_name = "brave_new_tab_ui" + output_dir = "$root_gen_dir/brave/components/brave_new_tab_ui" deps = [ ":brave_new_tab_ui" ] } diff --git a/components/brave_new_tab_ui/brave_new_tab.html b/components/brave_new_tab_ui/brave_new_tab.html index 8701520c371a..9eafbcd91b8e 100644 --- a/components/brave_new_tab_ui/brave_new_tab.html +++ b/components/brave_new_tab_ui/brave_new_tab.html @@ -13,7 +13,7 @@ - +