From 58bd1755b9237648f8e097cd6da82105763eea14 Mon Sep 17 00:00:00 2001 From: zenparsing Date: Mon, 28 Oct 2024 17:37:25 -0400 Subject: [PATCH] [NTP Next] Add updated NTP with background support --- browser/about_flags.cc | 8 + browser/brave_content_browser_client.cc | 3 + browser/ui/BUILD.gn | 11 + .../brave_new_tab/custom_image_chooser.cc | 86 ++++ .../brave_new_tab/custom_image_chooser.h | 53 +++ .../brave_new_tab/new_tab_page_handler.cc | 383 ++++++++++++++++++ .../brave_new_tab/new_tab_page_handler.h | 93 +++++ browser/ui/webui/brave_new_tab/new_tab_ui.cc | 94 +++++ browser/ui/webui/brave_new_tab/new_tab_ui.h | 50 +++ .../ui/webui/brave_new_tab/update_observer.cc | 48 +++ .../ui/webui/brave_new_tab/update_observer.h | 42 ++ .../webui/brave_web_ui_controller_factory.cc | 5 + .../ui/webui/new_tab_page/brave_new_tab_ui.cc | 6 +- components/brave_new_tab/common/BUILD.gn | 19 + components/brave_new_tab/common/features.cc | 14 + components/brave_new_tab/common/features.h | 17 + .../brave_new_tab/common/new_tab_page.mojom | 84 ++++ components/brave_new_tab/resources/BUILD.gn | 25 ++ .../resources/brave_new_tab_resources.grdp | 6 + .../resources/components/app.style.ts | 69 ++++ .../resources/components/app.tsx | 32 ++ .../resources/components/background.style.ts | 47 +++ .../resources/components/background.tsx | 88 ++++ .../settings/background_panel.style.ts | 113 ++++++ .../components/settings/background_panel.tsx | 307 ++++++++++++++ .../settings/settings_dialog.style.ts | 37 ++ .../components/settings/settings_dialog.tsx | 55 +++ .../brave_new_tab/resources/lib/background.ts | 80 ++++ .../resources/lib/inline_css_vars.ts | 12 + .../resources/lib/locale_context.ts | 44 ++ .../resources/lib/new_tab_context.ts | 20 + .../resources/lib/new_tab_model.ts | 89 ++++ .../brave_new_tab/resources/lib/scoped_css.ts | 72 ++++ .../resources/lib/state_manager.ts | 59 +++ .../resources/lib/use_callback_wrapper.ts | 32 ++ .../brave_new_tab/resources/lib/use_locale.ts | 12 + .../resources/lib/use_model_state.ts | 33 ++ .../brave_new_tab/resources/new_tab.html | 22 + .../brave_new_tab/resources/new_tab.tsx | 42 ++ .../brave_new_tab/resources/stories/index.tsx | 28 ++ .../stories/storybook_new_tab_model.ts | 101 +++++ .../resources/stories/storybook_strings.ts | 10 + .../brave_new_tab/resources/tsconfig.json | 9 + .../resources/webui/debouncer.ts | 17 + .../resources/webui/new_tab_mojom.ts | 30 ++ .../resources/webui/new_tab_proxy.ts | 23 ++ .../resources/webui/webui_new_tab_model.ts | 141 +++++++ components/brave_new_tab_ui/BUILD.gn | 8 +- .../brave_new_tab_ui/brave_new_tab.html | 2 +- components/resources/BUILD.gn | 2 + .../resources/brave_components_resources.grd | 4 +- resources/resource_ids.spec | 6 +- ui/webui/resources/BUILD.gn | 1 + 53 files changed, 2684 insertions(+), 10 deletions(-) create mode 100644 browser/ui/webui/brave_new_tab/custom_image_chooser.cc create mode 100644 browser/ui/webui/brave_new_tab/custom_image_chooser.h create mode 100644 browser/ui/webui/brave_new_tab/new_tab_page_handler.cc create mode 100644 browser/ui/webui/brave_new_tab/new_tab_page_handler.h create mode 100644 browser/ui/webui/brave_new_tab/new_tab_ui.cc create mode 100644 browser/ui/webui/brave_new_tab/new_tab_ui.h create mode 100644 browser/ui/webui/brave_new_tab/update_observer.cc create mode 100644 browser/ui/webui/brave_new_tab/update_observer.h create mode 100644 components/brave_new_tab/common/BUILD.gn create mode 100644 components/brave_new_tab/common/features.cc create mode 100644 components/brave_new_tab/common/features.h create mode 100644 components/brave_new_tab/common/new_tab_page.mojom create mode 100644 components/brave_new_tab/resources/BUILD.gn create mode 100644 components/brave_new_tab/resources/brave_new_tab_resources.grdp create mode 100644 components/brave_new_tab/resources/components/app.style.ts create mode 100644 components/brave_new_tab/resources/components/app.tsx create mode 100644 components/brave_new_tab/resources/components/background.style.ts create mode 100644 components/brave_new_tab/resources/components/background.tsx create mode 100644 components/brave_new_tab/resources/components/settings/background_panel.style.ts create mode 100644 components/brave_new_tab/resources/components/settings/background_panel.tsx create mode 100644 components/brave_new_tab/resources/components/settings/settings_dialog.style.ts create mode 100644 components/brave_new_tab/resources/components/settings/settings_dialog.tsx create mode 100644 components/brave_new_tab/resources/lib/background.ts create mode 100644 components/brave_new_tab/resources/lib/inline_css_vars.ts create mode 100644 components/brave_new_tab/resources/lib/locale_context.ts create mode 100644 components/brave_new_tab/resources/lib/new_tab_context.ts create mode 100644 components/brave_new_tab/resources/lib/new_tab_model.ts create mode 100644 components/brave_new_tab/resources/lib/scoped_css.ts create mode 100644 components/brave_new_tab/resources/lib/state_manager.ts create mode 100644 components/brave_new_tab/resources/lib/use_callback_wrapper.ts create mode 100644 components/brave_new_tab/resources/lib/use_locale.ts create mode 100644 components/brave_new_tab/resources/lib/use_model_state.ts create mode 100644 components/brave_new_tab/resources/new_tab.html create mode 100644 components/brave_new_tab/resources/new_tab.tsx create mode 100644 components/brave_new_tab/resources/stories/index.tsx create mode 100644 components/brave_new_tab/resources/stories/storybook_new_tab_model.ts create mode 100644 components/brave_new_tab/resources/stories/storybook_strings.ts create mode 100644 components/brave_new_tab/resources/tsconfig.json create mode 100644 components/brave_new_tab/resources/webui/debouncer.ts create mode 100644 components/brave_new_tab/resources/webui/new_tab_mojom.ts create mode 100644 components/brave_new_tab/resources/webui/new_tab_proxy.ts create mode 100644 components/brave_new_tab/resources/webui/webui_new_tab_model.ts diff --git a/browser/about_flags.cc b/browser/about_flags.cc index 09fa0f7aeeb6..85118d3babda 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/common/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" @@ -962,6 +963,13 @@ "corners, padding, and a drop shadow", \ kOsWin | kOsLinux | kOsMac, \ FEATURE_VALUE_TYPE(features::kBraveWebViewRoundedCorners), \ + }, \ + { \ + "brave-use-updated-ntp", \ + "Use the updated New Tab Page", \ + "Uses an updated version of the New Tab Page", \ + kOsWin | kOsLinux | kOsMac, \ + FEATURE_VALUE_TYPE(brave_new_tab::features::kUseUpdatedNTP), \ }) \ BRAVE_NATIVE_WALLET_FEATURE_ENTRIES \ BRAVE_NEWS_FEATURE_ENTRIES \ diff --git a/browser/brave_content_browser_client.cc b/browser/brave_content_browser_client.cc index cab7d23a6675..2d81f5bade88 100644 --- a/browser/brave_content_browser_client.cc +++ b/browser/brave_content_browser_client.cc @@ -224,6 +224,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_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" @@ -824,6 +825,8 @@ void BraveContentBrowserClient::RegisterBrowserInterfaceBindersForFrame( content::RegisterWebUIControllerInterfaceBinder< commands::mojom::CommandsService, BraveSettingsUI>(map); } + content::RegisterWebUIControllerInterfaceBinder< + brave_new_tab::mojom::NewTabPageHandler, brave_new_tab::NewTabUI>(map); #endif auto* prefs = diff --git a/browser/ui/BUILD.gn b/browser/ui/BUILD.gn index 4f2e3c2b89f3..95ef7dfe5e06 100644 --- a/browser/ui/BUILD.gn +++ b/browser/ui/BUILD.gn @@ -240,6 +240,14 @@ source_set("ui") { "toolbar/brave_bookmark_sub_menu_model.cc", "toolbar/brave_bookmark_sub_menu_model.h", "toolbar/brave_recent_tabs_sub_menu_model.h", + "webui/brave_new_tab/custom_image_chooser.cc", + "webui/brave_new_tab/custom_image_chooser.h", + "webui/brave_new_tab/new_tab_page_handler.cc", + "webui/brave_new_tab/new_tab_page_handler.h", + "webui/brave_new_tab/new_tab_ui.cc", + "webui/brave_new_tab/new_tab_ui.h", + "webui/brave_new_tab/update_observer.cc", + "webui/brave_new_tab/update_observer.h", "webui/brave_rewards/rewards_page_top_ui.cc", "webui/brave_rewards/rewards_page_top_ui.h", "webui/brave_rewards/rewards_panel_handler.cc", @@ -327,6 +335,9 @@ source_set("ui") { "//base", "//brave/browser/profiles:util", "//brave/common", + "//brave/components/brave_new_tab/common", + "//brave/components/brave_new_tab/common:mojom", + "//brave/components/brave_new_tab/resources:generated_resources", "//brave/components/constants", "//brave/components/misc_metrics", "//brave/components/sidebar/browser", 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..ead70b7349c5 --- /dev/null +++ b/browser/ui/webui/brave_new_tab/new_tab_page_handler.cc @@ -0,0 +1,383 @@ +// 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 "base/barrier_callback.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 "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::kCampaignIdKey; + using ntp_background_images::kCreativeInstanceIDKey; + using ntp_background_images::kIsBackgroundKey; + 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; + } + + 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, + bool is_restored_page) + : receiver_(this, std::move(receiver)), + update_observer_(pref_service, + base::BindRepeating(&NewTabPageHandler::OnUpdate, + base::Unretained(this))), + 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), + page_restored_(is_restored_page) {} + +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::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: + background->type = mojom::SelectedBackgroundType::kSolid; + if (!bg_prefs.ShouldUseRandomValue()) { + // This could be a gradient, but there's no way to know without parsing + // the value. We currently don't store whether the user's selection is a + // solid or gradient if they have provided a value. + background->value = bg_prefs.GetSelectedValue(); + } else if (bg_prefs.GetSelectedValue() == "gradient") { + background->type = mojom::SelectedBackgroundType::kGradient; + } + break; + } + + std::move(callback).Run(std::move(background)); +} + +void NewTabPageHandler::GetSponsoredImageBackground( + GetSponsoredImageBackgroundCallback callback) { + // TODO(zenparsing): Can view_counter_service_ be null? + auto data = page_restored_ + ? view_counter_service_->GetNextWallpaperForDisplay() + : view_counter_service_->GetCurrentWallpaperForDisplay(); + + if (!data) { + std::move(callback).Run(nullptr); + return; + } + + 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, + base::Unretained(this), 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, + base::Unretained(this), 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, + base::Unretained(this), 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); + } + + 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..e0fa224b1697 --- /dev/null +++ b/browser/ui/webui/brave_new_tab/new_tab_page_handler.h @@ -0,0 +1,93 @@ +// 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 "brave/browser/ui/webui/brave_new_tab/update_observer.h" +#include "brave/components/brave_new_tab/common/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, + bool is_restored_page); + + ~NewTabPageHandler() override; + + // mojom::NewTabPageHandler: + void SetNewTabPage(mojo::PendingRemote page) override; + void GetBackgroundsEnabled(GetBackgroundsEnabledCallback callback) override; + void SetBackgroundsEnabled(bool enabled, + SetBackgroundsEnabledCallback 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_; + bool page_restored_ = false; +}; + +} // 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_ui.cc b/browser/ui/webui/brave_new_tab/new_tab_ui.cc new file mode 100644 index 000000000000..e1b9b023987e --- /dev/null +++ b/browser/ui/webui/brave_new_tab/new_tab_ui.cc @@ -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/. + +#include "brave/browser/ui/webui/brave_new_tab/new_tab_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/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/strings/grit/components_strings.h" +#include "content/public/browser/navigation_entry.h" +#include "content/public/browser/web_contents.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 { + +// Adds support for displaying images stored in the custom background image +// folder. +void AddCustomImageDataSource(Profile* profile) { + auto* custom_background_service = + BraveNTPCustomBackgroundServiceFactory::GetForContext(profile); + if (!custom_background_service) { + return; + } + auto source = std::make_unique( + custom_background_service); + content::URLDataSource::Add(profile, std::move(source)); +} + +} // namespace + +NewTabUI::NewTabUI(content::WebUI* web_ui) : ui::MojoWebUIController(web_ui) { + auto* profile = Profile::FromWebUI(web_ui); + + auto* source = content::WebUIDataSource::CreateAndAdd( + Profile::FromWebUI(web_ui), chrome::kChromeUINewTabHost); + + webui::SetupWebUIDataSource(source, kBraveNewTabGenerated, + IDR_BRAVE_NEW_TAB_HTML); + + AddBackgroundColorToSource(source, web_ui->GetWebContents()); + + source->OverrideContentSecurityPolicy( + network::mojom::CSPDirectiveName::ImgSrc, + "img-src chrome://resources chrome://theme chrome://background-wallpaper " + "chrome://custom-wallpaper chrome://favicon2 blob: data: 'self';"); + + AddCustomImageDataSource(profile); + + web_ui->OverrideTitle( + brave_l10n::GetLocalizedResourceUTF16String(IDS_NEW_TAB_TITLE)); + + // static constexpr webui::LocalizedString kStrings[] = { + // {"headerText", IDS_WELCOME_HEADER}}; + // source->AddLocalizedStrings(kStrings); +} + +NewTabUI::~NewTabUI() = default; + +void NewTabUI::BindInterface( + mojo::PendingReceiver pending_receiver) { + auto* navigation_entry = + web_ui()->GetWebContents()->GetController().GetLastCommittedEntry(); + + 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), + navigation_entry ? navigation_entry->IsRestored() : false); +} + +WEB_UI_CONTROLLER_TYPE_IMPL(NewTabUI) + +} // namespace brave_new_tab diff --git a/browser/ui/webui/brave_new_tab/new_tab_ui.h b/browser/ui/webui/brave_new_tab/new_tab_ui.h new file mode 100644 index 000000000000..f16ca78e0c2c --- /dev/null +++ b/browser/ui/webui/brave_new_tab/new_tab_ui.h @@ -0,0 +1,50 @@ +// 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_UI_H_ +#define BRAVE_BROWSER_UI_WEBUI_BRAVE_NEW_TAB_NEW_TAB_UI_H_ + +#include + +#include "brave/components/brave_new_tab/common/new_tab_page.mojom.h" +#include "chrome/common/webui_url_constants.h" +#include "content/public/browser/webui_config.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 NewTabUI : public ui::MojoWebUIController { + public: + explicit NewTabUI(content::WebUI* web_ui); + ~NewTabUI() override; + + void BindInterface( + mojo::PendingReceiver pending_receiver); + + private: + std::unique_ptr page_handler_; + + WEB_UI_CONTROLLER_TYPE_DECL(); +}; + +// TODO(zenparsing): Currently unused. There is a complex interaction between +// the old new tab page and the private new tab page that makes using this +// implementation non-trivial. Eventually this UI will handle both designs. +class NewTabUIConfig : public content::DefaultWebUIConfig { + public: + NewTabUIConfig() + : DefaultWebUIConfig(content::kChromeUIScheme, + chrome::kChromeUINewTabHost) {} +}; + +} // namespace brave_new_tab + +#endif // BRAVE_BROWSER_UI_WEBUI_BRAVE_NEW_TAB_NEW_TAB_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..726f1a431e99 --- /dev/null +++ b/browser/ui/webui/brave_new_tab/update_observer.cc @@ -0,0 +1,48 @@ +// 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 "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, + base::RepeatingCallback callback) + : callback_(callback) { + CHECK(pref_service); + CHECK(callback_); + + 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::OnUpdate(Source update_source) { + 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, + base::Unretained(this), 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..ce3f14df0c01 --- /dev/null +++ b/browser/ui/webui/brave_new_tab/update_observer.h @@ -0,0 +1,42 @@ +// 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 "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 }; + + UpdateObserver(PrefService* pref_service, + base::RepeatingCallback callback); + ~UpdateObserver(); + + UpdateObserver(const UpdateObserver&) = delete; + UpdateObserver& operator=(const UpdateObserver&) = delete; + + 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_; +}; + +} // 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 6538b57eb32f..ec5cdb172b4a 100644 --- a/browser/ui/webui/brave_web_ui_controller_factory.cc +++ b/browser/ui/webui/brave_web_ui_controller_factory.cc @@ -20,6 +20,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/common/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" @@ -39,6 +40,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_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" @@ -141,6 +143,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::NewTabUI(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 7f5d13c09e09..994b7bf4b1e0 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" @@ -68,8 +68,8 @@ BraveNewTabUI::BraveNewTabUI(content::WebUI* web_ui, const std::string& name) // Non blank NTP. content::WebUIDataSource* source = CreateAndAddWebUIDataSource( - web_ui, name, kBraveNewTabGenerated, kBraveNewTabGeneratedSize, - IDR_BRAVE_NEW_TAB_HTML); + web_ui, name, kBraveNewTabUiGenerated, kBraveNewTabUiGeneratedSize, + IDR_BRAVE_NEW_TAB_UI_HTML); AddBackgroundColorToSource(source, web_contents); diff --git a/components/brave_new_tab/common/BUILD.gn b/components/brave_new_tab/common/BUILD.gn new file mode 100644 index 000000000000..410345394753 --- /dev/null +++ b/components/brave_new_tab/common/BUILD.gn @@ -0,0 +1,19 @@ +# 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("common") { + sources = [ + "features.cc", + "features.h", + ] + + deps = [ "//base" ] +} + +mojom("mojom") { + sources = [ "new_tab_page.mojom" ] +} diff --git a/components/brave_new_tab/common/features.cc b/components/brave_new_tab/common/features.cc new file mode 100644 index 000000000000..9161ed890217 --- /dev/null +++ b/components/brave_new_tab/common/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/common/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/common/features.h b/components/brave_new_tab/common/features.h new file mode 100644 index 000000000000..2c5afe8500e6 --- /dev/null +++ b/components/brave_new_tab/common/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_COMMON_FEATURES_H_ +#define BRAVE_COMPONENTS_BRAVE_NEW_TAB_COMMON_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_COMMON_FEATURES_H_ diff --git a/components/brave_new_tab/common/new_tab_page.mojom b/components/brave_new_tab/common/new_tab_page.mojom new file mode 100644 index 000000000000..9424b47649e7 --- /dev/null +++ b/components/brave_new_tab/common/new_tab_page.mojom @@ -0,0 +1,84 @@ +// 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 SponsoredImageBackground { + string wallpaper_id; + string creative_instance_id; + string campaign_id; + string image_url; +}; + +// 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) => (); + + // 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/resources/BUILD.gn b/components/brave_new_tab/resources/BUILD.gn new file mode 100644 index 000000000000..05ab171b8ef3 --- /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("brave_new_tab") { + entry_points = [ [ + "new_tab", + rebase_path("new_tab.tsx"), + ] ] + resource_name = "brave_new_tab" + output_module = true + deps = [ "//brave/components/brave_new_tab/common:mojom_js" ] +} + +pack_web_resources("generated_resources") { + resource_name = "brave_new_tab" + output_dir = "$root_gen_dir/brave/components/brave_new_tab/resources" + deps = [ ":brave_new_tab" ] +} 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..ddc51ac01737 --- /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/components/app.style.ts b/components/brave_new_tab/resources/components/app.style.ts new file mode 100644 index 000000000000..584d37f4ffb1 --- /dev/null +++ b/components/brave_new_tab/resources/components/app.style.ts @@ -0,0 +1,69 @@ +/* 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; + } + } + +` + +global('global-app-styles').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..5f1939cb52f3 --- /dev/null +++ b/components/brave_new_tab/resources/components/app.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 Icon from '@brave/leo/react/icon' + +import { Background } from './background' +import { SettingsDialog } from './settings/settings_dialog' + +import { style } from './app.style' + +export function App() { + const [showSettings, setShowSettings] = React.useState(false) + + return ( +
+ + + { + showSettings && + setShowSettings(false)} /> + } +
+ ) +} 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..f2f66d4dc3bf --- /dev/null +++ b/components/brave_new_tab/resources/components/background.tsx @@ -0,0 +1,88 @@ +/* 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 '../lib/new_tab_context' + +import { style } from './background.style' + +// Loads an image in the background and resolves when the image has either +// loaded or was unable to load. +function loadImageInBackground(url: string): Promise { + return new Promise((resolve) => { + if (!url) { + resolve() + } + const onReady = () => { + image.removeEventListener('load', onReady) + image.removeEventListener('error', onReady) + resolve() + } + const image = new Image() + image.addEventListener('load', onReady) + image.addEventListener('error', onReady) + image.src = url + }) +} + +function setBackgroundVariable(value: string) { + if (value) { + document.body.style.setProperty('--ntp-background', value) + } else { + document.body.style.removeProperty('--ntp-background') + } +} + +function ImageBackground(props: { url: string }) { + // In order to avoid a "flash-of-unloaded-image", load the image in the + // background and only update the background CSS variable when the image has + // finished loading. + React.useEffect(() => { + let cancel = false + loadImageInBackground(props.url).then(() => { + if (!cancel) { + setBackgroundVariable(`url(${CSS.escape(props.url)})`) + } + }) + return () => { cancel = true } + }, [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/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..def97cc9aafa --- /dev/null +++ b/components/brave_new_tab/resources/components/settings/background_panel.tsx @@ -0,0 +1,307 @@ +/* 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 '../../lib/new_tab_model' +import { useNewTabModel, useNewTabState } from '../../lib/new_tab_context' +import { useCallbackWrapper } from '../../lib/use_callback_wrapper' +import { inlineCSSVars } from '../../lib/inline_css_vars' + +import { + solidBackgrounds, + solidPreviewBackground, + gradientBackgrounds, + gradientPreviewBackground } from '../../lib/background' + +import { style } from './background_panel.style' + +export function BackgroundPanel() { + const model = useNewTabModel() + const wrapCallback = useCallbackWrapper() + + const [ + backgroundsEnabled, + sponsoredImagesEnabled, + selectedBackgroundType, + selectedBackground, + braveBackgrounds, + customBackgrounds, + currentBackground + ] = useNewTabState((state) => [ + state.backgroundsEnabled, + state.sponsoredImagesEnabled, + state.selectedBackgroundType, + state.selectedBackground, + state.braveBackgrounds, + state.customBackgrounds, + state.currentBackground + ]) + + const [panelType, setPanelType] = React.useState('none') + const [uploading, setUploading] = React.useState(false) + + function cssValue(type: BackgroundType, value: string) { + switch (type) { + case 'brave': + case 'custom': return `url(${CSS.escape(value)})` + case 'solid': + case 'gradient': + case 'none': return value + } + } + + 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 ? : } + Upload from device +
+ ) + } + + function renderSelectedMarker() { + return ( + + + + ) + } + + function renderRemoveImageButton(value: string) { + return ( + + ) + } + + function renderTypePreview(type: BackgroundType) { + if (type === 'custom' && customBackgrounds.length === 0) { + return renderUploadPreview() + } + const cssVars = inlineCSSVars({ + '--preview-background': cssValue(type, getTypePreviewValue(type)) + }) + return ( +
+ {type === selectedBackgroundType && renderSelectedMarker()} +
+ ) + } + + function panelTitle(type: BackgroundType) { + switch (type) { + case 'custom': return 'Use your own' + case 'gradient': return 'Gradient colors' + case 'solid': return 'Solid colors' + default: return '' + } + } + + function panelValues(type: BackgroundType) { + switch (type) { + case 'custom': return customBackgrounds + case 'gradient': return gradientBackgrounds + case 'solid': return solidBackgrounds + default: return [] + } + } + + function showCustomBackgroundChooser() { + model.showCustomBackgroundChooser().then(wrapCallback((selected) => { + if (selected) { + setUploading(true) + model.addCustomBackgrounds().then(wrapCallback(() => { + setUploading(false) + })) + } + })) + } + + function renderPanel(type: BackgroundType) { + const values = panelValues(type) + return ( +
+

+ +

+
+ + { + if (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 + } + } + }} + /> +
+
+ { + values.map((value) => { + const isSelected = + selectedBackgroundType === type && + selectedBackground === value + + const classNames = ['background-option'] + if (type === 'custom') { + classNames.push('can-remove') + } + + const cssVars = inlineCSSVars({ + '--preview-background': cssValue(type, value) + }) + + return ( +
+ + {type === 'custom' && renderRemoveImageButton(value)} +
+ ) + }) + } + { + type === 'custom' && + + } +
+
+ ) + } + + function onCustomPreviewClick() { + if (customBackgrounds.length === 0) { + showCustomBackgroundChooser() + } else { + setPanelType('custom') + } + } + + function renderTypeOptions() { + if (!backgroundsEnabled) { + return null + } + return ( +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ ) + } + + function renderSponsoredImageToggle() { + if (!backgroundsEnabled) { + return null + } + return ( +
+ + { + model.setSponsoredImagesEnabled(checked) + }} + /> +
+ ) + } + + if (panelType !== 'none') { + return renderPanel(panelType) + } + + return ( +
+
+ + { model.setBackgroundsEnabled(checked) }} + /> +
+ {renderTypeOptions()} + {renderSponsoredImageToggle()} +
+ ) +} 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..2cdcbc021aba --- /dev/null +++ b/components/brave_new_tab/resources/components/settings/settings_dialog.style.ts @@ -0,0 +1,37 @@ +/* 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}; + } + + 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..d1cbb918f58d --- /dev/null +++ b/components/brave_new_tab/resources/components/settings/settings_dialog.tsx @@ -0,0 +1,55 @@ +/* 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 { style } from './settings_dialog.style' + +interface Props { + onClose: () => void +} + +type PanelType = 'background' + +export function SettingsDialog(props: Props) { + const [currentPanel, setCurrentPanel] = + React.useState('background') + + function renderPanel() { + switch (currentPanel) { + case 'background': return + } + } + + return ( +
+ +

+ Customize Dashboard +

+
+ +
+ {renderPanel()} +
+
+
+
+ ) +} diff --git a/components/brave_new_tab/resources/lib/background.ts b/components/brave_new_tab/resources/lib/background.ts new file mode 100644 index 000000000000..312360ee570b --- /dev/null +++ b/components/brave_new_tab/resources/lib/background.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/. */ + +import { NewTabState, Background } 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 } = state + + if (!backgroundsEnabled) { + return defaultBackground + } + + if (sponsoredImageBackground) { + return sponsoredImageBackground + } + + 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 + } + } +} 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..61cee7d07434 --- /dev/null +++ b/components/brave_new_tab/resources/lib/inline_css_vars.ts @@ -0,0 +1,12 @@ +/* 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 +} + +export function inlineCSSVars(vars: CSSVars) { + return vars as React.CSSProperties +} diff --git a/components/brave_new_tab/resources/lib/locale_context.ts b/components/brave_new_tab/resources/lib/locale_context.ts new file mode 100644 index 000000000000..865659ddc243 --- /dev/null +++ b/components/brave_new_tab/resources/lib/locale_context.ts @@ -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' + +export interface Locale { + getString: (key: K) => string + getPluralString: (key: K, count: number) => Promise +} + +export const LocaleContext = React.createContext({ + getString: () => '', + getPluralString: async () => '' +}) + +export function useLocale(): Locale { + return React.useContext(LocaleContext) +} + +export function usePluralString( + key: K, + count: number | undefined | null +) { + const { getPluralString } = useLocale() + const [value, setValue] = React.useState('') + + React.useEffect(() => { + if (typeof count !== 'number') { + setValue('') + return + } + let canUpdate = true + getPluralString(key, count).then((newValue) => { + if (canUpdate) { + setValue(newValue) + } + }) + return () => { canUpdate = false } + }, [getPluralString, count]) + + return value +} diff --git a/components/brave_new_tab/resources/lib/new_tab_context.ts b/components/brave_new_tab/resources/lib/new_tab_context.ts new file mode 100644 index 000000000000..84178df65205 --- /dev/null +++ b/components/brave_new_tab/resources/lib/new_tab_context.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/. */ + +import * as React from 'react' + +import { NewTabModel, NewTabState, defaultModel } from './new_tab_model' +import { useModelState } from './use_model_state' + +export const NewTabModelContext = + React.createContext(defaultModel()) + +export function useNewTabModel(): NewTabModel { + return React.useContext(NewTabModelContext) +} + +export function useNewTabState(map: (state: NewTabState) => T): T { + return useModelState(useNewTabModel(), map) +} diff --git a/components/brave_new_tab/resources/lib/new_tab_model.ts b/components/brave_new_tab/resources/lib/new_tab_model.ts new file mode 100644 index 000000000000..7edc531549f7 --- /dev/null +++ b/components/brave_new_tab/resources/lib/new_tab_model.ts @@ -0,0 +1,89 @@ +/* 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' + +interface BraveBackground { + type: 'brave' + author: string + imageUrl: string + link: string +} + +interface ColorBackground { + type: 'solid' | 'gradient' + cssValue: string +} + +interface CustomBackground { + type: 'custom' + imageUrl: string +} + +interface SponsoredImageBackground { + type: 'sponsored' + imageUrl: string, + creativeInstanceId: string + wallpaperId: string +} + +export type Background = + BraveBackground | + ColorBackground | + CustomBackground | + SponsoredImageBackground + +export interface NewTabState { + backgroundsEnabled: boolean + sponsoredImagesEnabled: boolean + braveBackgrounds: BraveBackground[] + customBackgrounds: string[] + selectedBackgroundType: BackgroundType + selectedBackground: string + currentBackground: Background | null + sponsoredImageBackground: SponsoredImageBackground | null +} + +export function defaultState(): NewTabState { + return { + backgroundsEnabled: true, + sponsoredImagesEnabled: true, + braveBackgrounds: [], + customBackgrounds: [], + selectedBackgroundType: 'none', + selectedBackground: '', + currentBackground: null, + sponsoredImageBackground: null, + } +} + +export interface NewTabModel { + getState: () => NewTabState + addListener: (listener: (state: NewTabState) => void) => () => void + getString: (key: string) => string + getPluralString: (key: string, count: number) => Promise + 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 () => {} }, + getString(key) { return '' }, + async getPluralString(key, count) { 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/lib/scoped_css.ts b/components/brave_new_tab/resources/lib/scoped_css.ts new file mode 100644 index 000000000000..aa107168008b --- /dev/null +++ b/components/brave_new_tab/resources/lib/scoped_css.ts @@ -0,0 +1,72 @@ +/* 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/. */ + +const stylesheetMap = new Map() + +// 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(id: string, cssText: unknown) { + if (!id) { + throw new Error('Argument "id" cannot be empty') + } + + let stylesheet = stylesheetMap.get(id) + if (!stylesheet) { + stylesheet = new CSSStyleSheet() + stylesheetMap.set(id, stylesheet) + 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 = 0xa + +// 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(attr.selector, ` + @scope (${attr.selector}) to ([${scopeAttributeName}]) { + ${String.raw(callsite, ...values)} + } + `) + return attr + } +} + +// Returns a template tag that adds global CSS to the document. The "id" +// argument must be unique. +export function global(id: string) { + return { + css(callsite: TemplateStringsArray, ...values: any[]) { + addStyles(id, String.raw(callsite, ...values)) + } + } +} diff --git a/components/brave_new_tab/resources/lib/state_manager.ts b/components/brave_new_tab/resources/lib/state_manager.ts new file mode 100644 index 000000000000..0ade594b04db --- /dev/null +++ b/components/brave_new_tab/resources/lib/state_manager.ts @@ -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/. */ + +type Listener = (state: State) => void + +export interface StateManager { + getState: () => State + update: (source: Partial) => void + addListener: (listener: Listener) => (() => void) +} + +export function createStateManager ( + initialState: State +): StateManager { + const listeners = new Set>() + const state = { ...initialState } + + let sendInitialState = false + + function getState() { + return state + } + + function update(source: Partial) { + for (const [key, value] of Object.entries(source)) { + if (value !== undefined) { + (state as any)[key] = value + } + } + sendInitialState = false + for (const listener of listeners) { + try { + listener(state) + } catch (e) { + // Rethrow error in a new microtask to prevent + // listeners from interfering with each other + queueMicrotask(() => { throw e }) + } + } + } + + function addListener(listener: Listener) { + if (!listeners.has(listener)) { + listeners.add(listener) + // Send initial state to listeners in a microtask + sendInitialState = true + queueMicrotask(() => { + if (sendInitialState && listeners.has(listener)) { + listener(state) + } + }) + } + return () => { listeners.delete(listener) } + } + + return { getState, update, addListener } +} 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_locale.ts b/components/brave_new_tab/resources/lib/use_locale.ts new file mode 100644 index 000000000000..3d002a562e1b --- /dev/null +++ b/components/brave_new_tab/resources/lib/use_locale.ts @@ -0,0 +1,12 @@ +/* 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 localeAPI from './locale_context' + +export type StringKey = 'placeholder' + +export const useLocale = localeAPI.useLocale + +export const usePluralString = localeAPI.usePluralString 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..99ad5d88a455 --- /dev/null +++ b/components/brave_new_tab/resources/lib/use_model_state.ts @@ -0,0 +1,33 @@ +/* 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 +} + +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/new_tab.html b/components/brave_new_tab/resources/new_tab.html new file mode 100644 index 000000000000..072d0c84ea10 --- /dev/null +++ b/components/brave_new_tab/resources/new_tab.html @@ -0,0 +1,22 @@ + + + + + + New Tab + + + + + + + + + +
+ + diff --git a/components/brave_new_tab/resources/new_tab.tsx b/components/brave_new_tab/resources/new_tab.tsx new file mode 100644 index 000000000000..3b88afb2c02c --- /dev/null +++ b/components/brave_new_tab/resources/new_tab.tsx @@ -0,0 +1,42 @@ +/* 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 './lib/locale_context' +import { NewTabModelContext } from './lib/new_tab_context' +import { createNewTabModel } from './webui/webui_new_tab_model' +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() + const root = createRoot(document.getElementById('root')!) + + root.render( + + + + + + ) + + Object.assign(self, { + [Symbol.for('newTabModel')]: newTabModel + }) +}) 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..dd2da9515e62 --- /dev/null +++ b/components/brave_new_tab/resources/stories/index.tsx @@ -0,0 +1,28 @@ +/* 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 '../lib/locale_context' +import { NewTabModelContext } from '../lib/new_tab_context' +import { createNewTabModel } from './storybook_new_tab_model' +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/storybook_new_tab_model.ts b/components/brave_new_tab/resources/stories/storybook_new_tab_model.ts new file mode 100644 index 000000000000..9e5becc8747b --- /dev/null +++ b/components/brave_new_tab/resources/stories/storybook_new_tab_model.ts @@ -0,0 +1,101 @@ +/* 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 { createStateManager } from '../lib/state_manager' +import { getCurrentBackground } from '../lib/background' +import { localeStrings } from './storybook_strings' + +import { + NewTabState, + NewTabModel, + defaultModel, + defaultState } from '../lib/new_tab_model' + +function delay(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms) + }) +} + +function createLocale(strings: any) { + const getString = + (key: string) => String(strings && strings[key] || 'MISSING') + return { + getString, + async getPluralString (key: string, count: number) { + return getString(key).replace('#', String(count)) + } + } +} + +export function createNewTabModel(): NewTabModel { + const locale = createLocale(localeStrings) + + const stateManager = createStateManager({ + ...defaultState(), + selectedBackground: + 'linear-gradient(125.83deg, #392DD1 0%, #A91B78 99.09%)', + selectedBackgroundType: 'gradient', + currentBackground: { + type: 'gradient', + cssValue: 'linear-gradient(125.83deg, #392DD1 0%, #A91B78 99.09%)' + }, + }) + + return { + ...defaultModel(), + + getString: locale.getString, + getPluralString: locale.getPluralString, + getState: stateManager.getState, + addListener: stateManager.addListener, + + setBackgroundsEnabled(enabled) { + stateManager.update({ backgroundsEnabled: enabled }) + }, + + setSponsoredImagesEnabled(enabled) { + stateManager.update({ sponsoredImagesEnabled: enabled }) + }, + + selectBackground(type, value) { + stateManager.update({ + selectedBackgroundType: type, + selectedBackground: value + }) + stateManager.update({ + currentBackground: getCurrentBackground(stateManager.getState()) + }) + }, + + async showCustomBackgroundChooser() { + return true + }, + + async addCustomBackgrounds() { + await delay(2000) + const { customBackgrounds } = stateManager.getState() + const url = + 'https://brave.com/static-assets/images/brave-logo-sans-text.svg' + customBackgrounds.push(url) + stateManager.update({ + customBackgrounds, + selectedBackground: url, + selectedBackgroundType: 'custom' + }) + stateManager.update({ + currentBackground: getCurrentBackground(stateManager.getState()) + }) + }, + + async removeCustomBackground(background) { + const { customBackgrounds } = stateManager.getState() + stateManager.update({ + customBackgrounds: + customBackgrounds.filter((elem) => elem !== background) + }) + }, + } +} diff --git a/components/brave_new_tab/resources/stories/storybook_strings.ts b/components/brave_new_tab/resources/stories/storybook_strings.ts new file mode 100644 index 000000000000..11649dea7bc1 --- /dev/null +++ b/components/brave_new_tab/resources/stories/storybook_strings.ts @@ -0,0 +1,10 @@ +/* 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/use_locale' + +export const localeStrings: { [K in StringKey]: string } = { + placeholder: '' +} diff --git a/components/brave_new_tab/resources/tsconfig.json b/components/brave_new_tab/resources/tsconfig.json new file mode 100644 index 000000000000..604d24d9998e --- /dev/null +++ b/components/brave_new_tab/resources/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig", + "include": [ + "**/*.ts", + "**/*.tsx", + "**/*.d.ts", + "../../../definitions/*.d.ts" + ] +} diff --git a/components/brave_new_tab/resources/webui/debouncer.ts b/components/brave_new_tab/resources/webui/debouncer.ts new file mode 100644 index 000000000000..702621871143 --- /dev/null +++ b/components/brave_new_tab/resources/webui/debouncer.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 debounceEvent( + fn: (...args: [...T]) => void +) { + let timeout: any + return (...args: [...T]) => { + clearTimeout(timeout) + timeout = setTimeout(() => { + timeout = undefined + fn(...args) + }, 30) + } +} diff --git a/components/brave_new_tab/resources/webui/new_tab_mojom.ts b/components/brave_new_tab/resources/webui/new_tab_mojom.ts new file mode 100644 index 000000000000..d558fd9bb2e6 --- /dev/null +++ b/components/brave_new_tab/resources/webui/new_tab_mojom.ts @@ -0,0 +1,30 @@ +/* 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 { BackgroundType } from '../lib/new_tab_model' + +import * as mojom from 'gen/brave/components/brave_new_tab/common/new_tab_page.mojom.m.js' + +export * from 'gen/brave/components/brave_new_tab/common/new_tab_page.mojom.m.js' + +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 + } +} diff --git a/components/brave_new_tab/resources/webui/new_tab_proxy.ts b/components/brave_new_tab/resources/webui/new_tab_proxy.ts new file mode 100644 index 000000000000..1dbc2bae62a1 --- /dev/null +++ b/components/brave_new_tab/resources/webui/new_tab_proxy.ts @@ -0,0 +1,23 @@ +/* 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 './new_tab_mojom' + +interface BrowserProxy { + callbackRouter: mojom.NewTabPageCallbackRouter + pageHandler: mojom.NewTabPageHandlerRemote +} + +let instance: BrowserProxy | null + +export function getNewTabProxy() { + if (!instance) { + const callbackRouter = new mojom.NewTabPageCallbackRouter() + const pageHandler = mojom.NewTabPageHandler.getRemote() + pageHandler.setNewTabPage(callbackRouter.$.bindNewPipeAndPassRemote()) + instance = { pageHandler, callbackRouter } + } + return instance +} 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..b66765a3e8fc --- /dev/null +++ b/components/brave_new_tab/resources/webui/webui_new_tab_model.ts @@ -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 { loadTimeData } from 'chrome://resources/js/load_time_data.js' + +import { NewTabModel , NewTabState, defaultState } from '../lib/new_tab_model' +import { getNewTabProxy } from './new_tab_proxy' +import { createStateManager } from '../lib/state_manager' +import { getCurrentBackground } from '../lib/background' +import { backgroundTypeFromMojo, backgroundTypeToMojo } from './new_tab_mojom' +import { debounceEvent } from './debouncer' + +export function createNewTabModel(): NewTabModel { + const { pageHandler, callbackRouter } = getNewTabProxy() + const stateManager = createStateManager(defaultState()) + + function updateCurrentBackground() { + stateManager.update({ + currentBackground: getCurrentBackground(stateManager.getState()) + }) + } + + async function updateBackgroundsEnabled() { + const { enabled } = await pageHandler.getBackgroundsEnabled() + stateManager.update({ backgroundsEnabled: enabled }) + } + + async function updateSponsoredImagesEnabled() { + const { enabled } = await pageHandler.getSponsoredImagesEnabled() + stateManager.update({ sponsoredImagesEnabled: enabled }) + } + + async function updateBraveBackgrounds() { + const { backgrounds } = await pageHandler.getBraveBackgrounds() + stateManager.update({ + braveBackgrounds: backgrounds.map((item) => ({ type: 'brave', ...item })) + }) + } + + async function updateSelectedBackground() { + const { background } = await pageHandler.getSelectedBackground() + if (background) { + stateManager.update({ + selectedBackgroundType: backgroundTypeFromMojo(background.type), + selectedBackground: background.value + }) + } else { + stateManager.update({ + selectedBackgroundType: 'none', + selectedBackground: '' + }) + } + } + + async function updateCustomBackgrounds() { + const { backgrounds } = await pageHandler.getCustomBackgrounds() + stateManager.update({ customBackgrounds: backgrounds }) + } + + async function updateSponsoredImageBackground() { + const { background } = await pageHandler.getSponsoredImageBackground() + if (background) { + stateManager.update({ + sponsoredImageBackground: { type: 'sponsored', ...background } + }) + } else { + stateManager.update({ sponsoredImageBackground: null }) + } + } + + async function loadData() { + await Promise.all([ + updateBackgroundsEnabled(), + updateSponsoredImagesEnabled(), + updateBraveBackgrounds(), + updateCustomBackgrounds(), + updateSelectedBackground(), + updateSponsoredImageBackground() + ]) + + updateCurrentBackground() + } + + callbackRouter.onBackgroundPrefsUpdated.addListener( + debounceEvent(async () => { + await Promise.all([ + updateCustomBackgrounds(), + updateSelectedBackground(), + ]) + updateCurrentBackground() + })) + + loadData() + + return { + getState: stateManager.getState, + + addListener: stateManager.addListener, + + getString(key) { + return loadTimeData.getString(key) + }, + + async getPluralString(key, count) { + throw new Error('Not implemented') + }, + + setBackgroundsEnabled(enabled) { + stateManager.update({ backgroundsEnabled: enabled }) + pageHandler.setBackgroundsEnabled(enabled) + }, + + setSponsoredImagesEnabled(enabled) { + stateManager.update({ sponsoredImagesEnabled: enabled }) + pageHandler.setSponsoredImagesEnabled(enabled) + }, + + selectBackground(type, value) { + stateManager.update({ + selectedBackgroundType: type, + selectedBackground: value + }) + pageHandler.selectBackground({ type: backgroundTypeToMojo(type), value }) + }, + + async showCustomBackgroundChooser() { + const { imagesSelected } = await pageHandler.showCustomBackgroundChooser() + return imagesSelected + }, + + async addCustomBackgrounds() { + await pageHandler.addCustomBackgrounds() + }, + + async removeCustomBackground(background) { + await pageHandler.removeCustomBackground(background) + }, + } +} diff --git a/components/brave_new_tab_ui/BUILD.gn b/components/brave_new_tab_ui/BUILD.gn index 75fa5ceb1a1f..c538892839f7 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 = [ @@ -21,12 +21,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 dcb687609c8d..96bb9a1f2236 100644 --- a/components/brave_new_tab_ui/brave_new_tab.html +++ b/components/brave_new_tab_ui/brave_new_tab.html @@ -12,7 +12,7 @@ - +