diff --git a/browser/about_flags.cc b/browser/about_flags.cc index 5d4f0f94d60c..a89b562ec01f 100644 --- a/browser/about_flags.cc +++ b/browser/about_flags.cc @@ -20,6 +20,7 @@ #include "brave/components/brave_ads/core/public/ad_units/notification_ad/notification_ad_feature.h" #include "brave/components/brave_ads/core/public/ads_feature.h" #include "brave/components/brave_component_updater/browser/features.h" +#include "brave/components/brave_new_tab/new_tab_features.h" #include "brave/components/brave_news/common/features.h" #include "brave/components/brave_rewards/common/buildflags/buildflags.h" #include "brave/components/brave_rewards/common/features.h" @@ -475,6 +476,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(brave_new_tab::features::kUseUpdatedNTP), \ + }, \ { \ "brave-adblock-cname-uncloaking", \ "Enable CNAME uncloaking", \ diff --git a/browser/brave_content_browser_client.cc b/browser/brave_content_browser_client.cc index 1201e8621f35..b4b931e01ceb 100644 --- a/browser/brave_content_browser_client.cc +++ b/browser/brave_content_browser_client.cc @@ -226,6 +226,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" @@ -840,6 +841,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/brave_profile_prefs.cc b/browser/brave_profile_prefs.cc index 23bf54eaf2d7..d116478c012c 100644 --- a/browser/brave_profile_prefs.cc +++ b/browser/brave_profile_prefs.cc @@ -9,7 +9,6 @@ #include "brave/browser/brave_shields/brave_shields_web_contents_observer.h" #include "brave/browser/ethereum_remote_client/buildflags/buildflags.h" -#include "brave/browser/new_tab/new_tab_shows_options.h" #include "brave/browser/themes/brave_dark_mode_utils.h" #include "brave/browser/translate/brave_translate_prefs_migration.h" #include "brave/browser/ui/bookmark/brave_bookmark_prefs.h" @@ -21,6 +20,7 @@ #include "brave/components/brave_ads/browser/analytics/p2a/p2a.h" #include "brave/components/brave_ads/core/public/prefs/obsolete_pref_util.h" #include "brave/components/brave_ads/core/public/prefs/pref_registry.h" +#include "brave/components/brave_new_tab/new_tab_prefs.h" #include "brave/components/brave_news/browser/brave_news_controller.h" #include "brave/components/brave_news/browser/brave_news_p3a.h" #include "brave/components/brave_news/browser/brave_news_pref_manager.h" @@ -371,9 +371,7 @@ void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry) { brave_private_new_tab::prefs::RegisterProfilePrefs(registry); #endif - registry->RegisterIntegerPref( - kNewTabPageShowsOptions, - static_cast(NewTabPageShowsOptions::kDashboard)); + brave_new_tab::prefs::RegisterProfilePrefs(registry); #if BUILDFLAG(ENABLE_CUSTOM_BACKGROUND) NTPBackgroundPrefs::RegisterPref(registry); diff --git a/browser/extensions/BUILD.gn b/browser/extensions/BUILD.gn index 240757b27579..cebbec169468 100644 --- a/browser/extensions/BUILD.gn +++ b/browser/extensions/BUILD.gn @@ -140,6 +140,7 @@ source_set("extensions") { "//brave/components/brave_ads/browser", "//brave/components/brave_ads/core/public:headers", "//brave/components/brave_extension:static_resources", + "//brave/components/brave_new_tab", "//brave/components/brave_news/common", "//brave/components/brave_rewards/browser", "//brave/components/brave_rewards/common", diff --git a/browser/extensions/api/settings_private/brave_prefs_util.cc b/browser/extensions/api/settings_private/brave_prefs_util.cc index c3fc2d0ca927..18e9be6a2d67 100644 --- a/browser/extensions/api/settings_private/brave_prefs_util.cc +++ b/browser/extensions/api/settings_private/brave_prefs_util.cc @@ -7,6 +7,7 @@ #include "brave/browser/ui/tabs/brave_tab_prefs.h" #include "brave/components/ai_chat/core/common/pref_names.h" +#include "brave/components/brave_new_tab/new_tab_prefs.h" #include "brave/components/brave_news/common/pref_names.h" #include "brave/components/brave_rewards/common/pref_names.h" #include "brave/components/brave_shields/core/common/pref_names.h" @@ -182,7 +183,7 @@ const PrefsUtil::TypedPrefMap& BravePrefsUtil::GetAllowlistedKeys() { settings_api::PrefType::kBoolean; (*s_brave_allowlist)[kNewTabPageShowBraveVPN] = settings_api::PrefType::kBoolean; - (*s_brave_allowlist)[kNewTabPageShowsOptions] = + (*s_brave_allowlist)[brave_new_tab::prefs::kNewTabShowsOption] = settings_api::PrefType::kNumber; #if BUILDFLAG(ENABLE_EXTENSIONS) // Web discovery prefs diff --git a/browser/new_tab/BUILD.gn b/browser/new_tab/BUILD.gn index b8d4834926e0..db318aadf909 100644 --- a/browser/new_tab/BUILD.gn +++ b/browser/new_tab/BUILD.gn @@ -30,6 +30,7 @@ source_set("unittest") { deps = [ "//brave/browser", "//brave/common", + "//brave/components/brave_new_tab", "//brave/components/constants", "//chrome/common", "//chrome/test:test_support", diff --git a/browser/new_tab/new_tab_shows_options.cc b/browser/new_tab/new_tab_shows_options.cc index 144f7641b80e..ae48bd15283e 100644 --- a/browser/new_tab/new_tab_shows_options.cc +++ b/browser/new_tab/new_tab_shows_options.cc @@ -7,6 +7,7 @@ #include +#include "brave/components/brave_new_tab/new_tab_prefs.h" #include "brave/components/constants/pref_names.h" #include "brave/components/constants/webui_url_constants.h" #include "brave/grit/brave_generated_resources.h" @@ -29,36 +30,32 @@ GURL GetNewTabPageURL(Profile* profile) { auto* prefs = profile->GetPrefs(); - NewTabPageShowsOptions option = static_cast( - prefs->GetInteger(kNewTabPageShowsOptions)); - if (option == NewTabPageShowsOptions::kHomepage) { - if (prefs->GetBoolean(prefs::kHomePageIsNewTabPage)) { + switch (brave_new_tab::prefs::GetNewTabShowsOption(prefs)) { + case brave_new_tab::prefs::NewTabShowsOption::kHomepage: + return prefs->GetBoolean(prefs::kHomePageIsNewTabPage) + ? GURL() + : GURL(prefs->GetString(prefs::kHomePage)); + case brave_new_tab::prefs::NewTabShowsOption::kBlankpage: + case brave_new_tab::prefs::NewTabShowsOption::kDashboard: return GURL(); - } - return GURL(prefs->GetString(prefs::kHomePage)); - } else if (option == NewTabPageShowsOptions::kBlankpage) { - // NewTab route will handle for blank page. - return GURL(); - } else { - DCHECK_EQ(NewTabPageShowsOptions::kDashboard, option); - return GURL(); } } base::Value::List GetNewTabShowsOptionsList(Profile* profile) { + using brave_new_tab::prefs::NewTabShowsOption; + base::Value::List list; base::Value::Dict dashboard_option; dashboard_option.Set("value", - static_cast(NewTabPageShowsOptions::kDashboard)); + static_cast(NewTabShowsOption::kDashboard)); dashboard_option.Set("name", l10n_util::GetStringUTF8( IDS_SETTINGS_NEW_TAB_NEW_TAB_PAGE_SHOWS_DASHBOARD)); list.Append(std::move(dashboard_option)); base::Value::Dict homepage_option; - homepage_option.Set("value", - static_cast(NewTabPageShowsOptions::kHomepage)); + homepage_option.Set("value", static_cast(NewTabShowsOption::kHomepage)); homepage_option.Set("name", l10n_util::GetStringUTF8( IDS_SETTINGS_NEW_TAB_NEW_TAB_PAGE_SHOWS_HOMEPAGE)); @@ -66,7 +63,7 @@ base::Value::List GetNewTabShowsOptionsList(Profile* profile) { base::Value::Dict blankpage_option; blankpage_option.Set("value", - static_cast(NewTabPageShowsOptions::kBlankpage)); + static_cast(NewTabShowsOption::kBlankpage)); blankpage_option.Set("name", l10n_util::GetStringUTF8( IDS_SETTINGS_NEW_TAB_NEW_TAB_PAGE_SHOWS_BLANKPAGE)); @@ -81,9 +78,8 @@ bool ShouldUseNewTabURLForNewTab(Profile* profile) { } bool ShouldNewTabShowDashboard(Profile* profile) { - auto* prefs = profile->GetPrefs(); - if (static_cast(prefs->GetInteger( - kNewTabPageShowsOptions)) == NewTabPageShowsOptions::kBlankpage) { + auto option = brave_new_tab::prefs::GetNewTabShowsOption(profile->GetPrefs()); + if (option == brave_new_tab::prefs::NewTabShowsOption::kBlankpage) { return false; } @@ -95,8 +91,8 @@ bool ShouldNewTabShowBlankpage(Profile* profile) { return false; } - return profile->GetPrefs()->GetInteger(kNewTabPageShowsOptions) == - static_cast(brave::NewTabPageShowsOptions::kBlankpage); + auto option = brave_new_tab::prefs::GetNewTabShowsOption(profile->GetPrefs()); + return option == brave_new_tab::prefs::NewTabShowsOption::kBlankpage; } } // namespace brave diff --git a/browser/new_tab/new_tab_shows_options.h b/browser/new_tab/new_tab_shows_options.h index 504d8a990416..61f70577edcf 100644 --- a/browser/new_tab/new_tab_shows_options.h +++ b/browser/new_tab/new_tab_shows_options.h @@ -13,12 +13,6 @@ class Profile; namespace brave { -enum class NewTabPageShowsOptions { - kDashboard, - kHomepage, - kBlankpage -}; - GURL GetNewTabPageURL(Profile* profile); base::Value::List GetNewTabShowsOptionsList(Profile* profile); bool ShouldUseNewTabURLForNewTab(Profile* profile); diff --git a/browser/new_tab/new_tab_shows_options_unittest.cc b/browser/new_tab/new_tab_shows_options_unittest.cc index 9a5fe7c028a6..baab5c888a78 100644 --- a/browser/new_tab/new_tab_shows_options_unittest.cc +++ b/browser/new_tab/new_tab_shows_options_unittest.cc @@ -4,6 +4,8 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "brave/browser/new_tab/new_tab_shows_options.h" + +#include "brave/components/brave_new_tab/new_tab_prefs.h" #include "brave/components/constants/pref_names.h" #include "brave/components/constants/webui_url_constants.h" #include "chrome/common/pref_names.h" @@ -15,6 +17,8 @@ #include "testing/gtest/include/gtest/gtest.h" #include "url/url_constants.h" +using brave_new_tab::prefs::NewTabShowsOption; + class BraveNewTabTest : public testing::Test { public: BraveNewTabTest() : manager_(TestingBrowserProcess::GetGlobal()) {} @@ -42,9 +46,8 @@ TEST_F(BraveNewTabTest, BasicTest) { auto* prefs = profile->GetPrefs(); // Check NTP url is empty for DASHBOARD. - prefs->SetInteger( - kNewTabPageShowsOptions, - static_cast(brave::NewTabPageShowsOptions::kDashboard)); + prefs->SetInteger(brave_new_tab::prefs::kNewTabShowsOption, + static_cast(NewTabShowsOption::kDashboard)); EXPECT_EQ(GURL(), brave::GetNewTabPageURL(profile)); EXPECT_EQ(GURL(), brave::GetNewTabPageURL(otr_profile)); EXPECT_TRUE(brave::ShouldUseNewTabURLForNewTab(profile)); @@ -52,9 +55,8 @@ TEST_F(BraveNewTabTest, BasicTest) { // Check NTP url is empty when option is HOMEPAGE and kHomePageIsNewTabPage // is true. - prefs->SetInteger( - kNewTabPageShowsOptions, - static_cast(brave::NewTabPageShowsOptions::kHomepage)); + prefs->SetInteger(brave_new_tab::prefs::kNewTabShowsOption, + static_cast(NewTabShowsOption::kHomepage)); prefs->SetString(prefs::kHomePage, "https://www.brave.com/"); prefs->SetBoolean(prefs::kHomePageIsNewTabPage, true); EXPECT_EQ(GURL(), brave::GetNewTabPageURL(profile)); @@ -77,9 +79,8 @@ TEST_F(BraveNewTabTest, BasicTest) { // Check NTP url is used when option is BLANKPAGE. // Blank page will go NTP route and BraveNewTabUI will handle it. - prefs->SetInteger( - kNewTabPageShowsOptions, - static_cast(brave::NewTabPageShowsOptions::kBlankpage)); + prefs->SetInteger(brave_new_tab::prefs::kNewTabShowsOption, + static_cast(NewTabShowsOption::kBlankpage)); EXPECT_EQ(GURL(), brave::GetNewTabPageURL(profile)); EXPECT_EQ(GURL(), brave::GetNewTabPageURL(otr_profile)); EXPECT_TRUE(brave::ShouldUseNewTabURLForNewTab(profile)); diff --git a/browser/resources/settings/brave_new_tab_page/brave_new_tab_browser_proxy.ts b/browser/resources/settings/brave_new_tab_page/brave_new_tab_browser_proxy.ts index 19ad1021fa91..ac1f3b695258 100644 --- a/browser/resources/settings/brave_new_tab_page/brave_new_tab_browser_proxy.ts +++ b/browser/resources/settings/brave_new_tab_page/brave_new_tab_browser_proxy.ts @@ -7,7 +7,7 @@ import {sendWithPromise} from 'chrome://resources/js/cr.js'; export type NewTabOption = { name: string - value: number // corresponds to NewTabPageShowsOptions enum + value: number // corresponds to NewTabShowsOption enum } export interface BraveNewTabBrowserProxy { diff --git a/browser/sources.gni b/browser/sources.gni index c03361868c48..0a869fc0790f 100644 --- a/browser/sources.gni +++ b/browser/sources.gni @@ -152,6 +152,7 @@ brave_chrome_browser_deps = [ "//brave/components/brave_ads/core", "//brave/components/brave_component_updater/browser", "//brave/components/brave_federated", + "//brave/components/brave_new_tab", "//brave/components/brave_new_tab_ui:mojom", "//brave/components/brave_news/common:mojom", "//brave/components/brave_perf_predictor/browser", @@ -289,6 +290,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", diff --git a/browser/ui/BUILD.gn b/browser/ui/BUILD.gn index cd400c78f3d8..5b8b13f25523 100644 --- a/browser/ui/BUILD.gn +++ b/browser/ui/BUILD.gn @@ -778,6 +778,7 @@ source_set("ui") { "//brave/components/brave_ads/core", "//brave/components/brave_federated", "//brave/components/brave_federated/public/interfaces", + "//brave/components/brave_new_tab", "//brave/components/brave_news/browser", "//brave/components/brave_news/common", "//brave/components/brave_perf_predictor/common", 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..a2caf8e05c46 --- /dev/null +++ b/browser/ui/webui/brave_new_tab/BUILD.gn @@ -0,0 +1,45 @@ +# Copyright (c) 2024 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") + +assert(!is_android) +assert(enable_custom_background) + +source_set("brave_new_tab") { + sources = [ + "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 = [ + "//brave/browser:browser_process", + "//brave/browser/ntp_background", + "//brave/components/brave_new_tab", + "//brave/components/brave_new_tab:mojom", + "//brave/components/brave_new_tab/resources: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/custom_image_chooser.cc b/browser/ui/webui/brave_new_tab/custom_image_chooser.cc new file mode 100644 index 000000000000..c6b6cdcb856a --- /dev/null +++ b/browser/ui/webui/brave_new_tab/custom_image_chooser.cc @@ -0,0 +1,86 @@ +// Copyright (c) 2024 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_)); + + 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..9601a1fae1e3 --- /dev/null +++ b/browser/ui/webui/brave_new_tab/custom_image_chooser.h @@ -0,0 +1,53 @@ +// Copyright (c) 2024 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_ptr.h" +#include "ui/shell_dialogs/select_file_dialog.h" + +class Profile; + +namespace content { +class WebContents; +} + +namespace brave_new_tab { + +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_ptr web_contents_ = nullptr; + raw_ptr profile_ = nullptr; + 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_handler.cc b/browser/ui/webui/brave_new_tab/new_tab_page_handler.cc new file mode 100644 index 000000000000..31763d3ce4a7 --- /dev/null +++ b/browser/ui/webui/brave_new_tab/new_tab_page_handler.cc @@ -0,0 +1,421 @@ +// Copyright (c) 2024 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 + +#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/browser/ui/webui/brave_new_tab/custom_image_chooser.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 "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 { + +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; +} + +} // namespace + +NewTabPageHandler::NewTabPageHandler( + mojo::PendingReceiver receiver, + std::unique_ptr custom_image_chooser, + std::unique_ptr custom_file_manager, + PrefService* pref_service, + ntp_background_images::ViewCounterService* view_counter_service) + : receiver_(this, std::move(receiver)), + update_observer_(pref_service), + custom_image_chooser_(std::move(custom_image_chooser)), + custom_file_manager_(std::move(custom_file_manager)), + pref_service_(pref_service), + view_counter_service_(view_counter_service) { + CHECK(custom_image_chooser_); + CHECK(custom_file_manager_); + CHECK(pref_service_); + + 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) { + auto* service = g_brave_browser_process->ntp_background_images_service(); + if (!service) { + std::move(callback).Run({}); + return; + } + + auto* image_data = service->GetBackgroundImagesData(); + if (!image_data || !image_data->IsValid()) { + std::move(callback).Run({}); + 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)); + } + + std::move(callback).Run(std::move(backgrounds)); +} + +void NewTabPageHandler::GetCustomBackgrounds( + GetCustomBackgroundsCallback callback) { + auto backgrounds = NTPBackgroundPrefs(pref_service_).GetCustomImageList(); + for (auto& background : backgrounds) { + background = GetCustomImageURL(background); + } + std::move(callback).Run(std::move(backgrounds)); +} + +void NewTabPageHandler::GetSelectedBackground( + GetSelectedBackgroundCallback callback) { + auto background = mojom::SelectedBackground::New(); + + NTPBackgroundPrefs bg_prefs(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; + } + + std::move(callback).Run(std::move(background)); +} + +void NewTabPageHandler::GetSponsoredImageBackground( + GetSponsoredImageBackgroundCallback callback) { + if (!view_counter_service_) { + std::move(callback).Run(nullptr); + return; + } + + auto data = view_counter_service_->GetCurrentWallpaperForDisplay(); + if (!data) { + std::move(callback).Run(nullptr); + return; + } + + 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); + } + + std::move(callback).Run(std::move(sponsored_image)); +} + +void NewTabPageHandler::SelectBackground( + mojom::SelectedBackgroundPtr background, + SelectBackgroundCallback callback) { + bool random = background->value.empty(); + std::string pref_value = background->value; + + auto bg_prefs = NTPBackgroundPrefs(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); + + 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::AddCustomBackgrounds( + AddCustomBackgroundsCallback callback) { + // Move the chosen image paths into a local variable. + std::vector image_paths = std::move(custom_image_paths_); + + // 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( + image_paths.size(), + base::BindOnce(&NewTabPageHandler::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 : image_paths) { + custom_file_manager_->SaveImage(path, copy_path.Then(on_image_saved)); + } +} + +void NewTabPageHandler::RemoveCustomBackground( + const std::string& background_url, + RemoveCustomBackgroundCallback 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(&NewTabPageHandler::OnCustomBackgroundRemoved, + weak_factory_.GetWeakPtr(), std::move(callback), + file_path)); +} + +void NewTabPageHandler::OnCustomBackgroundsSelected( + ShowCustomBackgroundChooserCallback callback, + std::vector paths) { + custom_image_paths_ = std::move(paths); + std::move(callback).Run(!custom_image_paths_.empty()); +} + +void NewTabPageHandler::OnCustomBackgroundsSaved( + AddCustomBackgroundsCallback callback, + std::vector paths) { + auto bg_prefs = NTPBackgroundPrefs(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 NewTabPageHandler::OnCustomBackgroundRemoved( + RemoveCustomBackgroundCallback callback, + base::FilePath path, + bool success) { + if (!success) { + std::move(callback).Run(); + return; + } + + auto file_name = + CustomBackgroundFileManager::Converter(path).To(); + + auto bg_prefs = NTPBackgroundPrefs(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(); +} + +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..7cca6672ebdf --- /dev/null +++ b/browser/ui/webui/brave_new_tab/new_tab_page_handler.h @@ -0,0 +1,95 @@ +// Copyright (c) 2024 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_ptr.h" +#include "base/memory/weak_ptr.h" +#include "brave/browser/ui/webui/brave_new_tab/update_observer.h" +#include "brave/components/brave_new_tab/new_tab_page.mojom.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 CustomBackgroundFileManager; +class PrefService; + +namespace ntp_background_images { +class ViewCounterService; +} + +namespace brave_new_tab { + +class CustomImageChooser; + +class NewTabPageHandler : public mojom::NewTabPageHandler { + public: + NewTabPageHandler( + mojo::PendingReceiver receiver, + std::unique_ptr custom_image_chooser, + std::unique_ptr custom_file_manager, + PrefService* pref_service, + ntp_background_images::ViewCounterService* view_counter_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 AddCustomBackgrounds(AddCustomBackgroundsCallback callback) override; + void RemoveCustomBackground(const std::string& background_url, + RemoveCustomBackgroundCallback callback) override; + + private: + void OnCustomBackgroundsSelected(ShowCustomBackgroundChooserCallback callback, + std::vector paths); + + void OnCustomBackgroundsSaved(AddCustomBackgroundsCallback callback, + std::vector paths); + + void OnCustomBackgroundRemoved(RemoveCustomBackgroundCallback callback, + base::FilePath path, + bool success); + + void OnUpdate(UpdateObserver::Source update_source); + + mojo::Receiver receiver_; + mojo::Remote page_; + UpdateObserver update_observer_; + std::unique_ptr custom_image_chooser_; + std::unique_ptr custom_file_manager_; + raw_ptr pref_service_; + raw_ptr view_counter_service_; + std::vector custom_image_paths_; + 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..07316cbbef92 --- /dev/null +++ b/browser/ui/webui/brave_new_tab/new_tab_page_ui.cc @@ -0,0 +1,119 @@ +// Copyright (c) 2024 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/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/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/brave_new_tab/new_tab_prefs.h" +#include "brave/components/brave_new_tab/resources/grit/brave_new_tab_generated_map.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)); +} + +bool ShouldShowBlankPage(Profile* profile) { + if (!profile->IsRegularProfile()) { + return false; + } + auto shows_option = prefs::GetNewTabShowsOption(profile->GetPrefs()); + return shows_option == prefs::NewTabShowsOption::kBlankpage; +} + +} // 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 (ShouldShowBlankPage(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()); + + page_handler_ = std::make_unique( + std::move(pending_receiver), + std::make_unique(web_ui()->GetWebContents()), + std::make_unique(profile), + profile->GetPrefs(), + ntp_background_images::ViewCounterServiceFactory::GetForProfile(profile)); +} + +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..df5ac8aaeb5f --- /dev/null +++ b/browser/ui/webui/brave_new_tab/new_tab_page_ui.h @@ -0,0 +1,39 @@ +// Copyright (c) 2024 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/components/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..b3f705070ada --- /dev/null +++ b/browser/ui/webui/brave_new_tab/update_observer.cc @@ -0,0 +1,54 @@ +// Copyright (c) 2024 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) { + CHECK(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..4c71d04db87d --- /dev/null +++ b/browser/ui/webui/brave_new_tab/update_observer.h @@ -0,0 +1,45 @@ +// Copyright (c) 2024 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..b4e9a092c95c 100644 --- a/browser/ui/webui/brave_web_ui_controller_factory.cc +++ b/browser/ui/webui/brave_web_ui_controller_factory.cc @@ -23,6 +23,7 @@ #include "brave/browser/ui/webui/skus_internals_ui.h" #include "brave/components/ai_rewriter/common/buildflags/buildflags.h" #include "brave/components/brave_federated/features.h" +#include "brave/components/brave_new_tab/new_tab_features.h" #include "brave/components/brave_rewards/common/features.h" #include "brave/components/brave_wallet/common/common_utils.h" #include "brave/components/constants/pref_names.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(brave_new_tab::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/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/browser/ui/webui/settings/brave_appearance_handler.cc b/browser/ui/webui/settings/brave_appearance_handler.cc index e0f12bbfa5ad..1f2f8825bb14 100644 --- a/browser/ui/webui/settings/brave_appearance_handler.cc +++ b/browser/ui/webui/settings/brave_appearance_handler.cc @@ -11,6 +11,7 @@ #include "brave/browser/new_tab/new_tab_shows_options.h" #include "brave/browser/profiles/profile_util.h" #include "brave/browser/themes/brave_dark_mode_utils.h" +#include "brave/components/brave_new_tab/new_tab_prefs.h" #include "brave/components/constants/pref_names.h" #include "brave/components/ntp_background_images/common/pref_names.h" #include "chrome/browser/browser_process.h" @@ -36,7 +37,7 @@ void BraveAppearanceHandler::RegisterMessages() { profile_ = Profile::FromWebUI(web_ui()); profile_state_change_registrar_.Init(profile_->GetPrefs()); profile_state_change_registrar_.Add( - kNewTabPageShowsOptions, + brave_new_tab::prefs::kNewTabShowsOption, base::BindRepeating(&BraveAppearanceHandler::OnPreferenceChanged, base::Unretained(this))); profile_state_change_registrar_.Add( @@ -98,7 +99,8 @@ void BraveAppearanceHandler::OnBraveDarkModeChanged() { void BraveAppearanceHandler::OnPreferenceChanged(const std::string& pref_name) { if (IsJavascriptAllowed()) { - if (pref_name == kNewTabPageShowsOptions || pref_name == prefs::kHomePage || + if (pref_name == brave_new_tab::prefs::kNewTabShowsOption || + pref_name == prefs::kHomePage || pref_name == prefs::kHomePageIsNewTabPage) { FireWebUIListener( "show-new-tab-dashboard-settings-changed", diff --git a/components/brave_new_tab/BUILD.gn b/components/brave_new_tab/BUILD.gn new file mode 100644 index 000000000000..d171dfa4b2b9 --- /dev/null +++ b/components/brave_new_tab/BUILD.gn @@ -0,0 +1,24 @@ +# Copyright (c) 2024 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("//mojo/public/tools/bindings/mojom.gni") + +source_set("brave_new_tab") { + sources = [ + "new_tab_features.cc", + "new_tab_features.h", + "new_tab_prefs.cc", + "new_tab_prefs.h", + ] + + deps = [ + "//base", + "//components/prefs", + ] +} + +mojom("mojom") { + sources = [ "new_tab_page.mojom" ] +} diff --git a/components/brave_new_tab/new_tab_features.cc b/components/brave_new_tab/new_tab_features.cc new file mode 100644 index 000000000000..db049434a997 --- /dev/null +++ b/components/brave_new_tab/new_tab_features.cc @@ -0,0 +1,14 @@ +/* Copyright (c) 2024 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/components/brave_new_tab/new_tab_features.h" + +namespace brave_new_tab::features { + +BASE_FEATURE(kUseUpdatedNTP, + "BraveUseUpdatedNewTabPage", + base::FEATURE_DISABLED_BY_DEFAULT); + +} // namespace brave_new_tab::features diff --git a/components/brave_new_tab/new_tab_features.h b/components/brave_new_tab/new_tab_features.h new file mode 100644 index 000000000000..6d1a16da0682 --- /dev/null +++ b/components/brave_new_tab/new_tab_features.h @@ -0,0 +1,17 @@ +/* Copyright (c) 2024 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_COMPONENTS_BRAVE_NEW_TAB_NEW_TAB_FEATURES_H_ +#define BRAVE_COMPONENTS_BRAVE_NEW_TAB_NEW_TAB_FEATURES_H_ + +#include "base/feature_list.h" + +namespace brave_new_tab::features { + +BASE_DECLARE_FEATURE(kUseUpdatedNTP); + +} // namespace brave_new_tab::features + +#endif // BRAVE_COMPONENTS_BRAVE_NEW_TAB_NEW_TAB_FEATURES_H_ diff --git a/components/brave_new_tab/new_tab_page.mojom b/components/brave_new_tab/new_tab_page.mojom new file mode 100644 index 000000000000..3bdc50a39733 --- /dev/null +++ b/components/brave_new_tab/new_tab_page.mojom @@ -0,0 +1,95 @@ +// Copyright (c) 2024 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 remove 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. + ShowCustomBackgroundChooser() => (bool images_selected); + + // Adds the custom background images selected by the custom image background + // chooser. + AddCustomBackgrounds() => (); + + // Removes the specified custom image background from the list of available + // backgrounds. + RemoveCustomBackground(string background_url) => (); + +}; diff --git a/components/brave_new_tab/new_tab_prefs.cc b/components/brave_new_tab/new_tab_prefs.cc new file mode 100644 index 000000000000..192c4ee0c0de --- /dev/null +++ b/components/brave_new_tab/new_tab_prefs.cc @@ -0,0 +1,40 @@ +/* Copyright (c) 2024 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/components/brave_new_tab/new_tab_prefs.h" + +#include "components/prefs/pref_registry_simple.h" +#include "components/prefs/pref_service.h" + +namespace brave_new_tab::prefs { + +namespace { + +constexpr auto kDefaultNewTabShowsOption = NewTabShowsOption::kDashboard; + +} // namespace + +void RegisterProfilePrefs(PrefRegistrySimple* registry) { + CHECK(registry); + registry->RegisterIntegerPref(kNewTabShowsOption, + static_cast(kDefaultNewTabShowsOption)); +} + +NewTabShowsOption GetNewTabShowsOption(PrefService* pref_service) { + CHECK(pref_service); + switch (pref_service->GetInteger(kNewTabShowsOption)) { + case static_cast(NewTabShowsOption::kDashboard): + return NewTabShowsOption::kDashboard; + case static_cast(NewTabShowsOption::kHomepage): + return NewTabShowsOption::kHomepage; + case static_cast(NewTabShowsOption::kBlankpage): + return NewTabShowsOption::kBlankpage; + } + pref_service->SetInteger(kNewTabShowsOption, + static_cast(kDefaultNewTabShowsOption)); + return kDefaultNewTabShowsOption; +} + +} // namespace brave_new_tab::prefs diff --git a/components/brave_new_tab/new_tab_prefs.h b/components/brave_new_tab/new_tab_prefs.h new file mode 100644 index 000000000000..4c90e3fcdeba --- /dev/null +++ b/components/brave_new_tab/new_tab_prefs.h @@ -0,0 +1,29 @@ +/* Copyright (c) 2024 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_COMPONENTS_BRAVE_NEW_TAB_NEW_TAB_PREFS_H_ +#define BRAVE_COMPONENTS_BRAVE_NEW_TAB_NEW_TAB_PREFS_H_ + +#include + +class PrefRegistrySimple; +class PrefService; + +namespace brave_new_tab::prefs { + +// Determines what is displayed when a new tab is opened. +inline constexpr char kNewTabShowsOption[] = "brave.new_tab_page.shows_options"; + +// Registers Brave new tab profile prefs. +void RegisterProfilePrefs(PrefRegistrySimple* registry); + +enum class NewTabShowsOption { kDashboard, kHomepage, kBlankpage }; + +// Returns a value indicating what is displayed when a new tab is opened. +NewTabShowsOption GetNewTabShowsOption(PrefService* pref_service); + +} // namespace brave_new_tab::prefs + +#endif // BRAVE_COMPONENTS_BRAVE_NEW_TAB_NEW_TAB_PREFS_H_ diff --git a/components/brave_new_tab/resources/BUILD.gn b/components/brave_new_tab/resources/BUILD.gn new file mode 100644 index 000000000000..05923ca48b9e --- /dev/null +++ b/components/brave_new_tab/resources/BUILD.gn @@ -0,0 +1,25 @@ +# Copyright (c) 2024 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/components/brave_new_tab:mojom_js" ] +} + +pack_web_resources("generated_resources") { + resource_name = "brave_new_tab" + output_dir = "$root_gen_dir/brave/components/brave_new_tab/resources" + deps = [ ":resources" ] +} diff --git a/components/brave_new_tab/resources/brave_new_tab_resources.grdp b/components/brave_new_tab/resources/brave_new_tab_resources.grdp new file mode 100644 index 000000000000..eea09dade080 --- /dev/null +++ b/components/brave_new_tab/resources/brave_new_tab_resources.grdp @@ -0,0 +1,6 @@ + + + + + + diff --git a/components/brave_new_tab/resources/brave_new_tab_strings.grdp b/components/brave_new_tab/resources/brave_new_tab_strings.grdp new file mode 100644 index 000000000000..69911b5c01d3 --- /dev/null +++ b/components/brave_new_tab/resources/brave_new_tab_strings.grdp @@ -0,0 +1,47 @@ + + + + Background Image + + + + Brave backgrounds + + + + Use your own + + + + Gradients + + + + Photo by $1 + + + + Refresh on every new tab + + + + Customize Dashboard + + + + Show Background Images + + + + Show Sponsored Images + + + + Solid colors + + + + Upload from device + + + diff --git a/components/brave_new_tab/resources/components/app.style.ts b/components/brave_new_tab/resources/components/app.style.ts new file mode 100644 index 000000000000..6a9d74ef82dd --- /dev/null +++ b/components/brave_new_tab/resources/components/app.style.ts @@ -0,0 +1,94 @@ +/* Copyright (c) 2024 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/components/brave_new_tab/resources/components/app.tsx b/components/brave_new_tab/resources/components/app.tsx new file mode 100644 index 000000000000..d34c297770a0 --- /dev/null +++ b/components/brave_new_tab/resources/components/app.tsx @@ -0,0 +1,45 @@ +/* Copyright (c) 2024 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 { SettingsDialog, SettingsView } from './settings/settings_dialog' + +import { style } from './app.style' + +export function App() { + const [settingsView, setSettingsView] = + React.useState(null) + + return ( +
+ +
+
+
+
+ +
+
+
+ + { + settingsView && + setSettingsView(null)} + /> + } +
+ ) +} diff --git a/components/brave_new_tab/resources/components/background.style.ts b/components/brave_new_tab/resources/components/background.style.ts new file mode 100644 index 000000000000..b09f26cfbe03 --- /dev/null +++ b/components/brave_new_tab/resources/components/background.style.ts @@ -0,0 +1,47 @@ +/* Copyright (c) 2024 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/components/brave_new_tab/resources/components/background.tsx b/components/brave_new_tab/resources/components/background.tsx new file mode 100644 index 000000000000..3aabbe3ab68d --- /dev/null +++ b/components/brave_new_tab/resources/components/background.tsx @@ -0,0 +1,71 @@ +/* Copyright (c) 2024 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 { useCallbackWrapper } from '../lib/use_callback_wrapper' +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 }) { + const wrapCallback = useCallbackWrapper() + + // 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(wrapCallback((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/components/brave_new_tab/resources/components/background_caption.style.ts b/components/brave_new_tab/resources/components/background_caption.style.ts new file mode 100644 index 000000000000..a42a3c666f3e --- /dev/null +++ b/components/brave_new_tab/resources/components/background_caption.style.ts @@ -0,0 +1,47 @@ +/* Copyright (c) 2024 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/components/brave_new_tab/resources/components/background_caption.tsx b/components/brave_new_tab/resources/components/background_caption.tsx new file mode 100644 index 000000000000..66f5079cc569 --- /dev/null +++ b/components/brave_new_tab/resources/components/background_caption.tsx @@ -0,0 +1,79 @@ +/* Copyright (c) 2024 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 + } + 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/components/brave_new_tab/resources/components/locale_context.tsx b/components/brave_new_tab/resources/components/locale_context.tsx new file mode 100644 index 000000000000..dfc4ac68da7b --- /dev/null +++ b/components/brave_new_tab/resources/components/locale_context.tsx @@ -0,0 +1,59 @@ +/* Copyright (c) 2024 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/components/brave_new_tab/resources/components/new_tab_context.tsx b/components/brave_new_tab/resources/components/new_tab_context.tsx new file mode 100644 index 000000000000..a183899c42da --- /dev/null +++ b/components/brave_new_tab/resources/components/new_tab_context.tsx @@ -0,0 +1,32 @@ +/* Copyright (c) 2024 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/components/brave_new_tab/resources/components/settings/background_panel.style.ts b/components/brave_new_tab/resources/components/settings/background_panel.style.ts new file mode 100644 index 000000000000..80c7b1b9d761 --- /dev/null +++ b/components/brave_new_tab/resources/components/settings/background_panel.style.ts @@ -0,0 +1,113 @@ +/* Copyright (c) 2024 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/components/brave_new_tab/resources/components/settings/background_panel.tsx b/components/brave_new_tab/resources/components/settings/background_panel.tsx new file mode 100644 index 000000000000..d9eb2a1baaa2 --- /dev/null +++ b/components/brave_new_tab/resources/components/settings/background_panel.tsx @@ -0,0 +1,198 @@ +/* Copyright (c) 2024 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 { useCallbackWrapper } from '../../lib/use_callback_wrapper' +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 wrapCallback = useCallbackWrapper() + + 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) + + function getTypePreviewValue(type: BackgroundType) { + const isSelectedType = type === selectedBackgroundType + switch (type) { + case 'brave': + return braveBackgrounds.at(0)?.imageUrl || '' + case 'custom': + if (isSelectedType && selectedBackground) { + return selectedBackground + } + return customBackgrounds.at(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(wrapCallback( + (backgroundSelected) => { + if (backgroundSelected && !uploading) { + setUploading(true) + model.addCustomBackgrounds().then(wrapCallback(() => { + setUploading(false) + })) + } + })) + } + + 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/components/brave_new_tab/resources/components/settings/background_type_panel.tsx b/components/brave_new_tab/resources/components/settings/background_type_panel.tsx new file mode 100644 index 000000000000..164b1f4e1c17 --- /dev/null +++ b/components/brave_new_tab/resources/components/settings/background_type_panel.tsx @@ -0,0 +1,141 @@ +/* Copyright (c) 2024 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 { + 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 + + const classNames = ['background-option'] + if (type === 'custom') { + classNames.push('can-remove') + } + + return ( +
+ + { + type === 'custom' && + + } +
+ ) + })} + {type === 'custom' && props.renderUploadOption()} +
+ +} diff --git a/components/brave_new_tab/resources/components/settings/settings_dialog.style.ts b/components/brave_new_tab/resources/components/settings/settings_dialog.style.ts new file mode 100644 index 000000000000..a43df55cde3b --- /dev/null +++ b/components/brave_new_tab/resources/components/settings/settings_dialog.style.ts @@ -0,0 +1,39 @@ +/* Copyright (c) 2024 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/components/brave_new_tab/resources/components/settings/settings_dialog.tsx b/components/brave_new_tab/resources/components/settings/settings_dialog.tsx new file mode 100644 index 000000000000..3dcd17a17f4c --- /dev/null +++ b/components/brave_new_tab/resources/components/settings/settings_dialog.tsx @@ -0,0 +1,71 @@ +/* Copyright (c) 2024 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_dialog.style' + +export type SettingsView = 'background' + +interface Props { + initialView: SettingsView + onClose: () => void +} + +export function SettingsDialog(props: Props) { + const { getString } = useLocale() + + const [currentView, setCurrentView] = + React.useState(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/components/brave_new_tab/resources/lib/image_loader.ts b/components/brave_new_tab/resources/lib/image_loader.ts new file mode 100644 index 000000000000..a406d2b56808 --- /dev/null +++ b/components/brave_new_tab/resources/lib/image_loader.ts @@ -0,0 +1,38 @@ +/* Copyright (c) 2024 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/components/brave_new_tab/resources/lib/inline_css_vars.ts b/components/brave_new_tab/resources/lib/inline_css_vars.ts new file mode 100644 index 000000000000..20b14727477b --- /dev/null +++ b/components/brave_new_tab/resources/lib/inline_css_vars.ts @@ -0,0 +1,14 @@ +/* Copyright (c) 2024 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/components/brave_new_tab/resources/lib/locale_strings.ts b/components/brave_new_tab/resources/lib/locale_strings.ts new file mode 100644 index 000000000000..2e7c6ddbd8bb --- /dev/null +++ b/components/brave_new_tab/resources/lib/locale_strings.ts @@ -0,0 +1,20 @@ +/* Copyright (c) 2024 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/components/brave_new_tab/resources/lib/scoped_css.ts b/components/brave_new_tab/resources/lib/scoped_css.ts new file mode 100644 index 000000000000..1b499fe56097 --- /dev/null +++ b/components/brave_new_tab/resources/lib/scoped_css.ts @@ -0,0 +1,58 @@ +/* Copyright (c) 2024 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/components/brave_new_tab/resources/lib/store.ts b/components/brave_new_tab/resources/lib/store.ts new file mode 100644 index 000000000000..956749052fa0 --- /dev/null +++ b/components/brave_new_tab/resources/lib/store.ts @@ -0,0 +1,80 @@ +/* Copyright (c) 2024 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 looop. + 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 + queueMicrotask(() => { + notificationQueued = false + for (const listener of listeners) { + if (notificationQueued) { + break + } + try { + listener(state) + } catch (e) { + 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/components/brave_new_tab/resources/lib/url_sanitizer.ts b/components/brave_new_tab/resources/lib/url_sanitizer.ts new file mode 100644 index 000000000000..9f69d1956caf --- /dev/null +++ b/components/brave_new_tab/resources/lib/url_sanitizer.ts @@ -0,0 +1,17 @@ +/* Copyright (c) 2024 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/components/brave_new_tab/resources/lib/use_callback_wrapper.ts b/components/brave_new_tab/resources/lib/use_callback_wrapper.ts new file mode 100644 index 000000000000..f0e91b5e6b91 --- /dev/null +++ b/components/brave_new_tab/resources/lib/use_callback_wrapper.ts @@ -0,0 +1,32 @@ +/* Copyright (c) 2024 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' + +// A hook useful for integrating async APIs with component state. +// +// When setting up a promise callback from a component event handler, it might +// turn out that the component is dismounted before the promise resolves. In +// such a case, you may see the following warning: +// +// "Can't perform a React state update on an unmounted component." +// +// By wrapping callbacks with the function returned by `useCallbackWrapper`, the +// wrapped callback will not be executed after the component has dismounted. +export function useCallbackWrapper() { + const active = React.useRef(true) + React.useEffect(() => { + return () => { active.current = false } + }, []) + return (callback: (...args: [...T]) => R) => { + return (...args: [...T]) => { + if (active.current) { + return callback(...args) + } else { + return undefined + } + } + } +} diff --git a/components/brave_new_tab/resources/lib/use_model_state.ts b/components/brave_new_tab/resources/lib/use_model_state.ts new file mode 100644 index 000000000000..1d14a8651c30 --- /dev/null +++ b/components/brave_new_tab/resources/lib/use_model_state.ts @@ -0,0 +1,35 @@ +/* Copyright (c) 2024 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/components/brave_new_tab/resources/models/backgrounds.ts b/components/brave_new_tab/resources/models/backgrounds.ts new file mode 100644 index 000000000000..a070bc5ef182 --- /dev/null +++ b/components/brave_new_tab/resources/models/backgrounds.ts @@ -0,0 +1,97 @@ +/* Copyright (c) 2024 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 { + const index = Math.floor(Math.random() * list.length) + return list.at(index) || null +} + +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/components/brave_new_tab/resources/models/new_tab_model.ts b/components/brave_new_tab/resources/models/new_tab_model.ts new file mode 100644 index 000000000000..4b5d37cfdd2c --- /dev/null +++ b/components/brave_new_tab/resources/models/new_tab_model.ts @@ -0,0 +1,94 @@ +/* Copyright (c) 2024 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 + addCustomBackgrounds: () => 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 addCustomBackgrounds() {}, + async removeCustomBackground(background) {} + } +} diff --git a/components/brave_new_tab/resources/new_tab_page.html b/components/brave_new_tab/resources/new_tab_page.html new file mode 100644 index 000000000000..072d0c84ea10 --- /dev/null +++ b/components/brave_new_tab/resources/new_tab_page.html @@ -0,0 +1,22 @@ + + + + + + New Tab + + + + + + + + + +
+ + diff --git a/components/brave_new_tab/resources/new_tab_page.tsx b/components/brave_new_tab/resources/new_tab_page.tsx new file mode 100644 index 000000000000..267ef423f356 --- /dev/null +++ b/components/brave_new_tab/resources/new_tab_page.tsx @@ -0,0 +1,44 @@ +/* Copyright (c) 2024 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') + +function whenDocumentReady() { + return new Promise((resolve) => { + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => resolve()) + } else { + resolve() + } + }) +} + +whenDocumentReady().then(() => { + const newTabModel = createNewTabModel() + + Object.assign(self, { + [Symbol.for('ntpInternals')]: { + newTabModel + } + }) + + createRoot(document.getElementById('root')!).render( + + + + + + ) +}) diff --git a/components/brave_new_tab/resources/stories/index.tsx b/components/brave_new_tab/resources/stories/index.tsx new file mode 100644 index 000000000000..ade09044f1f4 --- /dev/null +++ b/components/brave_new_tab/resources/stories/index.tsx @@ -0,0 +1,29 @@ +/* Copyright (c) 2024 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/components/brave_new_tab/resources/stories/sb_locale.ts b/components/brave_new_tab/resources/stories/sb_locale.ts new file mode 100644 index 000000000000..3323aeb07e29 --- /dev/null +++ b/components/brave_new_tab/resources/stories/sb_locale.ts @@ -0,0 +1,34 @@ +/* Copyright (c) 2024 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/components/brave_new_tab/resources/stories/sb_new_tab_model.ts b/components/brave_new_tab/resources/stories/sb_new_tab_model.ts new file mode 100644 index 000000000000..fde0002bb535 --- /dev/null +++ b/components/brave_new_tab/resources/stories/sb_new_tab_model.ts @@ -0,0 +1,106 @@ +/* Copyright (c) 2024 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() { + return true + }, + + async addCustomBackgrounds() { + await delay(2000) + + store.update((state) => ({ + customBackgrounds: [...state.customBackgrounds, sampleBackground], + selectedBackground: sampleBackground, + selectedBackgroundType: 'custom' + })) + + store.update({ + currentBackground: getCurrentBackground(store.getState()) + }) + }, + + async removeCustomBackground(background) { + store.update((state) => ({ + customBackgrounds: + state.customBackgrounds.filter((elem) => elem !== background) + })) + } + } +} diff --git a/components/brave_new_tab/resources/tsconfig.json b/components/brave_new_tab/resources/tsconfig.json new file mode 100644 index 000000000000..3b6366185a85 --- /dev/null +++ b/components/brave_new_tab/resources/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig", + "include": [ + "**/*.ts", + "**/*.tsx", + "**/*.d.ts" + ] +} diff --git a/components/brave_new_tab/resources/webui/callback_listeners.ts b/components/brave_new_tab/resources/webui/callback_listeners.ts new file mode 100644 index 000000000000..d40e2e0981c7 --- /dev/null +++ b/components/brave_new_tab/resources/webui/callback_listeners.ts @@ -0,0 +1,15 @@ +/* Copyright (c) 2024 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 addCallbackListeners(router: any, listeners: Partial) { + for (const [key, value] of Object.entries(listeners)) { + router[key].addListener(value) + } + return () => { + for (const [key, value] of Object.entries(listeners)) { + router[key].removeListener(value) + } + } +} diff --git a/components/brave_new_tab/resources/webui/new_tab_page_proxy.ts b/components/brave_new_tab/resources/webui/new_tab_page_proxy.ts new file mode 100644 index 000000000000..b7673281872d --- /dev/null +++ b/components/brave_new_tab/resources/webui/new_tab_page_proxy.ts @@ -0,0 +1,29 @@ +/* Copyright (c) 2024 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/components/brave_new_tab/new_tab_page.mojom.m.js' + +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 + } + + 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/components/brave_new_tab/resources/webui/webui_locale.ts b/components/brave_new_tab/resources/webui/webui_locale.ts new file mode 100644 index 000000000000..a7d774f5dce2 --- /dev/null +++ b/components/brave_new_tab/resources/webui/webui_locale.ts @@ -0,0 +1,18 @@ +/* Copyright (c) 2024 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/components/brave_new_tab/resources/webui/webui_new_tab_model.ts b/components/brave_new_tab/resources/webui/webui_new_tab_model.ts new file mode 100644 index 000000000000..c9cdcd568d90 --- /dev/null +++ b/components/brave_new_tab/resources/webui/webui_new_tab_model.ts @@ -0,0 +1,157 @@ +/* Copyright (c) 2024 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/components/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' +import { addCallbackListeners } from './callback_listeners' + +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 { handler, callbackRouter} = NewTabPageProxy.getInstance() + 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 + }) + } + + addCallbackListeners(callbackRouter, { + 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 addCustomBackgrounds() { + await handler.addCustomBackgrounds() + }, + + async removeCustomBackground(background) { + await handler.removeCustomBackground(background) + } + } +} diff --git a/components/brave_new_tab_ui/BUILD.gn b/components/brave_new_tab_ui/BUILD.gn index 83fd783a67df..fe6d469cf0eb 100644 --- a/components/brave_new_tab_ui/BUILD.gn +++ b/components/brave_new_tab_ui/BUILD.gn @@ -8,7 +8,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 = [ @@ -22,12 +22,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 @@ - +