From e9bef8c9195f6a491e9d1e27ff200b209e7ebf46 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/sources.gni | 2 + browser/ui/BUILD.gn | 1 + browser/ui/config.gni | 6 +- browser/ui/webui/brave_new_tab/BUILD.gn | 44 ++ .../brave_new_tab/custom_image_chooser.cc | 86 ++++ .../brave_new_tab/custom_image_chooser.h | 53 +++ .../brave_new_tab/new_tab_page_handler.cc | 421 ++++++++++++++++++ .../brave_new_tab/new_tab_page_handler.h | 93 ++++ browser/ui/webui/brave_new_tab/new_tab_ui.cc | 110 +++++ browser/ui/webui/brave_new_tab/new_tab_ui.h | 39 ++ .../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 | 91 ++++ components/brave_new_tab/resources/BUILD.gn | 25 ++ .../resources/brave_new_tab_resources.grdp | 6 + .../resources/components/app.style.ts | 94 ++++ .../resources/components/app.tsx | 45 ++ .../resources/components/background.style.ts | 47 ++ .../resources/components/background.tsx | 71 +++ .../components/background_caption.style.ts | 47 ++ .../components/background_caption.tsx | 78 ++++ .../resources/components/locale_context.tsx | 59 +++ .../resources/components/new_tab_context.tsx | 32 ++ .../settings/background_panel.style.ts | 113 +++++ .../components/settings/background_panel.tsx | 193 ++++++++ .../settings/background_type_panel.tsx | 146 ++++++ .../settings/settings_dialog.style.ts | 39 ++ .../components/settings/settings_dialog.tsx | 71 +++ .../resources/lib/image_loader.ts | 38 ++ .../resources/lib/inline_css_vars.ts | 14 + .../resources/lib/locale_strings.ts | 20 + .../brave_new_tab/resources/lib/scoped_css.ts | 72 +++ .../brave_new_tab/resources/lib/store.ts | 80 ++++ .../resources/lib/use_callback_wrapper.ts | 32 ++ .../resources/lib/use_model_state.ts | 35 ++ .../resources/models/backgrounds.ts | 90 ++++ .../resources/models/new_tab_model.ts | 92 ++++ .../brave_new_tab/resources/new_tab.html | 22 + .../brave_new_tab/resources/new_tab.tsx | 42 ++ .../brave_new_tab/resources/stories/index.tsx | 29 ++ .../resources/stories/sb_locale.ts | 34 ++ .../resources/stories/sb_new_tab_model.ts | 106 +++++ .../brave_new_tab/resources/tsconfig.json | 9 + .../resources/webui/callback_listeners.ts | 15 + .../resources/webui/debouncer.ts | 17 + .../resources/webui/new_tab_page_proxy.ts | 29 ++ .../resources/webui/webui_locale.ts | 18 + .../resources/webui/webui_new_tab_model.ts | 151 +++++++ 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/brave_components_strings.grd | 1 + components/resources/new_tab_strings.grdp | 47 ++ resources/resource_ids.spec | 6 +- ui/webui/resources/BUILD.gn | 1 + 63 files changed, 3178 insertions(+), 12 deletions(-) create mode 100644 browser/ui/webui/brave_new_tab/BUILD.gn 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/background_caption.style.ts create mode 100644 components/brave_new_tab/resources/components/background_caption.tsx create mode 100644 components/brave_new_tab/resources/components/locale_context.tsx create mode 100644 components/brave_new_tab/resources/components/new_tab_context.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/background_type_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/image_loader.ts create mode 100644 components/brave_new_tab/resources/lib/inline_css_vars.ts create mode 100644 components/brave_new_tab/resources/lib/locale_strings.ts create mode 100644 components/brave_new_tab/resources/lib/scoped_css.ts create mode 100644 components/brave_new_tab/resources/lib/store.ts create mode 100644 components/brave_new_tab/resources/lib/use_callback_wrapper.ts create mode 100644 components/brave_new_tab/resources/lib/use_model_state.ts create mode 100644 components/brave_new_tab/resources/models/backgrounds.ts create mode 100644 components/brave_new_tab/resources/models/new_tab_model.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/sb_locale.ts create mode 100644 components/brave_new_tab/resources/stories/sb_new_tab_model.ts create mode 100644 components/brave_new_tab/resources/tsconfig.json create mode 100644 components/brave_new_tab/resources/webui/callback_listeners.ts create mode 100644 components/brave_new_tab/resources/webui/debouncer.ts create mode 100644 components/brave_new_tab/resources/webui/new_tab_page_proxy.ts create mode 100644 components/brave_new_tab/resources/webui/webui_locale.ts create mode 100644 components/brave_new_tab/resources/webui/webui_new_tab_model.ts create mode 100644 components/resources/new_tab_strings.grdp diff --git a/browser/about_flags.cc b/browser/about_flags.cc index c31ba23e2856..a7a02ef0c691 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" @@ -969,6 +970,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 b077b28c5e15..92c81d659ce6 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" @@ -836,6 +837,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/sources.gni b/browser/sources.gni index f715cdf8008f..789445f69941 100644 --- a/browser/sources.gni +++ b/browser/sources.gni @@ -151,6 +151,7 @@ brave_chrome_browser_deps = [ "//brave/components/brave_ads/core", "//brave/components/brave_component_updater/browser", "//brave/components/brave_federated", + "//brave/components/brave_new_tab/common", "//brave/components/brave_new_tab_ui:mojom", "//brave/components/brave_news/common:mojom", "//brave/components/brave_perf_predictor/browser", @@ -288,6 +289,7 @@ if (!is_android) { brave_chrome_browser_deps += [ "//brave/browser/ui/ai_chat", + "//brave/browser/ui/webui/brave_new_tab", "//brave/browser/ui/webui/brave_news_internals", "//components/feed:feature_list", "//components/feed/core/v2:feed_core_v2", diff --git a/browser/ui/BUILD.gn b/browser/ui/BUILD.gn index c04466aa2455..3e1d415e6b6c 100644 --- a/browser/ui/BUILD.gn +++ b/browser/ui/BUILD.gn @@ -777,6 +777,7 @@ source_set("ui") { "//brave/components/brave_ads/core", "//brave/components/brave_federated", "//brave/components/brave_federated/public/interfaces", + "//brave/components/brave_new_tab/common", "//brave/components/brave_news/browser", "//brave/components/brave_news/common", "//brave/components/brave_perf_predictor/common", diff --git a/browser/ui/config.gni b/browser/ui/config.gni index 62a9a4940769..dceee48b7c9a 100644 --- a/browser/ui/config.gni +++ b/browser/ui/config.gni @@ -5,6 +5,8 @@ brave_ui_allow_circular_includes_from = [] if (!is_android) { - brave_ui_allow_circular_includes_from += - [ "//brave/browser/ui/webui/brave_news_internals" ] + brave_ui_allow_circular_includes_from += [ + "//brave/browser/ui/webui/brave_new_tab", + "//brave/browser/ui/webui/brave_news_internals", + ] } diff --git a/browser/ui/webui/brave_new_tab/BUILD.gn b/browser/ui/webui/brave_new_tab/BUILD.gn new file mode 100644 index 000000000000..eae823554b4a --- /dev/null +++ b/browser/ui/webui/brave_new_tab/BUILD.gn @@ -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("//brave/components/ntp_background_images/buildflags/buildflags.gni") + +assert(!is_android) +assert(enable_custom_background) + +source_set("brave_new_tab") { + sources = [ + "custom_image_chooser.cc", + "custom_image_chooser.h", + "new_tab_page_handler.cc", + "new_tab_page_handler.h", + "new_tab_ui.cc", + "new_tab_ui.h", + "update_observer.cc", + "update_observer.h", + ] + + deps = [ + "//brave/browser:browser_process", + "//brave/browser/ntp_background", + "//brave/components/brave_new_tab/common", + "//brave/components/brave_new_tab/common:mojom", + "//brave/components/brave_new_tab/resources:generated_resources", + "//brave/components/l10n/common", + "//brave/components/ntp_background_images/browser", + "//brave/components/ntp_background_images/common", + "//brave/components/resources:static_resources", + "//brave/components/resources:strings", + "//chrome/app:generated_resources", + "//chrome/browser:browser_public_dependencies", + "//chrome/browser/profiles:profile", + "//chrome/browser/ui/webui:webui_util", + "//components/prefs", + "//components/strings:components_strings", + "//ui/base", + "//ui/shell_dialogs", + "//ui/webui", + ] +} diff --git a/browser/ui/webui/brave_new_tab/custom_image_chooser.cc b/browser/ui/webui/brave_new_tab/custom_image_chooser.cc new file mode 100644 index 000000000000..c6b6cdcb856a --- /dev/null +++ b/browser/ui/webui/brave_new_tab/custom_image_chooser.cc @@ -0,0 +1,86 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/browser/ui/webui/brave_new_tab/custom_image_chooser.h" + +#include + +#include "brave/components/l10n/common/localization_util.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/ui/chrome_select_file_policy.h" +#include "chrome/grit/generated_resources.h" +#include "content/public/browser/web_contents.h" +#include "ui/shell_dialogs/selected_file_info.h" + +namespace brave_new_tab { + +CustomImageChooser::CustomImageChooser(content::WebContents* web_contents) + : web_contents_(web_contents), + profile_(Profile::FromBrowserContext(web_contents->GetBrowserContext())) { +} + +CustomImageChooser::~CustomImageChooser() = default; + +void CustomImageChooser::ShowDialog(ShowDialogCallback callback) { + if (callback_) { + std::move(callback_).Run({}); + } + + callback_ = std::move(callback); + + if (dialog_) { + return; + } + + dialog_ = ui::SelectFileDialog::Create( + this, std::make_unique(web_contents_)); + + ui::SelectFileDialog::FileTypeInfo file_types; + file_types.allowed_paths = ui::SelectFileDialog::FileTypeInfo::NATIVE_PATH; + file_types.extensions.push_back( + {{FILE_PATH_LITERAL("jpg"), FILE_PATH_LITERAL("jpeg"), + FILE_PATH_LITERAL("png"), FILE_PATH_LITERAL("gif")}}); + file_types.extension_description_overrides.push_back( + brave_l10n::GetLocalizedResourceUTF16String(IDS_UPLOAD_IMAGE_FORMAT)); + + dialog_->SelectFile(ui::SelectFileDialog::SELECT_OPEN_MULTI_FILE, + std::u16string(), profile_->last_selected_directory(), + &file_types, 0, base::FilePath::StringType(), + web_contents_->GetTopLevelNativeWindow(), nullptr); +} + +void CustomImageChooser::FileSelected(const ui::SelectedFileInfo& file, + int index) { + dialog_ = nullptr; + profile_->set_last_selected_directory(file.path().DirName()); + if (callback_) { + std::move(callback_).Run({file.path()}); + } +} + +void CustomImageChooser::MultiFilesSelected( + const std::vector& files) { + dialog_ = nullptr; + if (!files.empty()) { + profile_->set_last_selected_directory(files.back().path().DirName()); + } + std::vector paths; + paths.reserve(files.size()); + for (auto& file : files) { + paths.push_back(file.path()); + } + if (callback_) { + std::move(callback_).Run(std::move(paths)); + } +} + +void CustomImageChooser::FileSelectionCanceled() { + dialog_ = nullptr; + if (callback_) { + std::move(callback_).Run({}); + } +} + +} // namespace brave_new_tab diff --git a/browser/ui/webui/brave_new_tab/custom_image_chooser.h b/browser/ui/webui/brave_new_tab/custom_image_chooser.h new file mode 100644 index 000000000000..9601a1fae1e3 --- /dev/null +++ b/browser/ui/webui/brave_new_tab/custom_image_chooser.h @@ -0,0 +1,53 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_BROWSER_UI_WEBUI_BRAVE_NEW_TAB_CUSTOM_IMAGE_CHOOSER_H_ +#define BRAVE_BROWSER_UI_WEBUI_BRAVE_NEW_TAB_CUSTOM_IMAGE_CHOOSER_H_ + +#include +#include + +#include "base/files/file_path.h" +#include "base/functional/callback.h" +#include "base/memory/raw_ptr.h" +#include "ui/shell_dialogs/select_file_dialog.h" + +class Profile; + +namespace content { +class WebContents; +} + +namespace brave_new_tab { + +class CustomImageChooser : public ui::SelectFileDialog::Listener { + public: + explicit CustomImageChooser(content::WebContents* web_contents); + ~CustomImageChooser() override; + + CustomImageChooser(const CustomImageChooser&) = delete; + CustomImageChooser& operator=(const CustomImageChooser&) = delete; + + using ShowDialogCallback = + base::OnceCallback)>; + + void ShowDialog(ShowDialogCallback callback); + + // ui::SelectFileDialog::Listener: + void FileSelected(const ui::SelectedFileInfo& file, int index) override; + void MultiFilesSelected( + const std::vector& files) override; + void FileSelectionCanceled() override; + + private: + raw_ptr web_contents_ = nullptr; + raw_ptr profile_ = nullptr; + scoped_refptr dialog_; + ShowDialogCallback callback_; +}; + +} // namespace brave_new_tab + +#endif // BRAVE_BROWSER_UI_WEBUI_BRAVE_NEW_TAB_CUSTOM_IMAGE_CHOOSER_H_ diff --git a/browser/ui/webui/brave_new_tab/new_tab_page_handler.cc b/browser/ui/webui/brave_new_tab/new_tab_page_handler.cc new file mode 100644 index 000000000000..dc74caeaccfe --- /dev/null +++ b/browser/ui/webui/brave_new_tab/new_tab_page_handler.cc @@ -0,0 +1,421 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/browser/ui/webui/brave_new_tab/new_tab_page_handler.h" + +#include +#include + +#include "base/barrier_callback.h" +#include "base/containers/contains.h" +#include "brave/browser/brave_browser_process.h" +#include "brave/browser/ntp_background/custom_background_file_manager.h" +#include "brave/browser/ntp_background/ntp_background_prefs.h" +#include "brave/browser/ui/webui/brave_new_tab/custom_image_chooser.h" +#include "brave/components/ntp_background_images/browser/ntp_background_images_data.h" +#include "brave/components/ntp_background_images/browser/ntp_background_images_service.h" +#include "brave/components/ntp_background_images/browser/url_constants.h" +#include "brave/components/ntp_background_images/browser/view_counter_service.h" +#include "brave/components/ntp_background_images/common/pref_names.h" +#include "components/prefs/pref_service.h" + +namespace brave_new_tab { + +namespace { + +std::string GetCustomImageURL(const std::string& image_name) { + return CustomBackgroundFileManager::Converter(image_name).To().spec(); +} + +std::string CustomImageNameFromURL(const std::string& url) { + return CustomBackgroundFileManager::Converter(GURL(url)).To(); +} + +mojom::SponsoredImageBackgroundPtr ReadSponsoredImageData( + const base::Value::Dict& data) { + using ntp_background_images::kAltKey; + using ntp_background_images::kCampaignIdKey; + using ntp_background_images::kCreativeInstanceIDKey; + using ntp_background_images::kDestinationURLKey; + using ntp_background_images::kImageKey; + using ntp_background_images::kIsBackgroundKey; + using ntp_background_images::kLogoKey; + using ntp_background_images::kWallpaperIDKey; + using ntp_background_images::kWallpaperImageURLKey; + + auto is_background = data.FindBool(kIsBackgroundKey); + if (is_background.value_or(false)) { + return nullptr; + } + + auto background = mojom::SponsoredImageBackground::New(); + + if (auto* creative_instance_id = data.FindString(kCreativeInstanceIDKey)) { + background->creative_instance_id = *creative_instance_id; + } + + if (auto* wallpaper_id = data.FindString(kWallpaperIDKey)) { + background->wallpaper_id = *wallpaper_id; + } + + if (auto* campaign_id = data.FindString(kCampaignIdKey)) { + background->campaign_id = *campaign_id; + } + + if (auto* image_url = data.FindString(kWallpaperImageURLKey)) { + background->image_url = *image_url; + } + + if (auto* logo_dict = data.FindDict(kLogoKey)) { + auto logo = mojom::SponsoredImageLogo::New(); + if (auto* alt = logo_dict->FindString(kAltKey)) { + logo->alt = *alt; + } + if (auto* destination_url = logo_dict->FindString(kDestinationURLKey)) { + logo->destination_url = *destination_url; + } + if (auto* image_url = logo_dict->FindString(kImageKey)) { + logo->image_url = *image_url; + } + if (!logo->image_url.empty()) { + background->logo = std::move(logo); + } + } + + return background; +} + +} // namespace + +NewTabPageHandler::NewTabPageHandler( + mojo::PendingReceiver receiver, + std::unique_ptr custom_image_chooser, + std::unique_ptr custom_file_manager, + PrefService* pref_service, + ntp_background_images::ViewCounterService* view_counter_service, + 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) { + CHECK(custom_image_chooser_); + CHECK(custom_file_manager_); + CHECK(pref_service_); +} + +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: + if (!bg_prefs.ShouldUseRandomValue()) { + background->value = bg_prefs.GetSelectedValue(); + background->type = base::Contains(background->value, "gradient") + ? mojom::SelectedBackgroundType::kGradient + : mojom::SelectedBackgroundType::kSolid; + } else if (bg_prefs.GetSelectedValue() == "gradient") { + background->type = mojom::SelectedBackgroundType::kGradient; + } else { + background->type = mojom::SelectedBackgroundType::kSolid; + } + break; + } + + std::move(callback).Run(std::move(background)); +} + +void NewTabPageHandler::GetSponsoredImageBackground( + GetSponsoredImageBackgroundCallback callback) { + if (!view_counter_service_) { + std::move(callback).Run(nullptr); + return; + } + + std::optional data; + + if (page_restored_) { + page_restored_ = false; + data = view_counter_service_->GetNextWallpaperForDisplay(); + } else { + data = view_counter_service_->GetCurrentWallpaperForDisplay(); + view_counter_service_->RegisterPageView(); + } + + 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); + bg_prefs.SetShouldUseRandomValue(false); + } + + std::move(callback).Run(); +} + +void NewTabPageHandler::OnCustomBackgroundRemoved( + RemoveCustomBackgroundCallback callback, + base::FilePath path, + bool success) { + if (!success) { + std::move(callback).Run(); + return; + } + + auto file_name = + CustomBackgroundFileManager::Converter(path).To(); + + auto bg_prefs = NTPBackgroundPrefs(pref_service_); + bg_prefs.RemoveCustomImageFromList(file_name); + + // If we are removing the currently selected background, either select the + // first remaining custom background, or, if there are none left, then select + // a default background. + if (bg_prefs.GetType() == NTPBackgroundPrefs::Type::kCustomImage && + bg_prefs.GetSelectedValue() == file_name) { + auto custom_images = bg_prefs.GetCustomImageList(); + if (custom_images.empty()) { + bg_prefs.SetType(NTPBackgroundPrefs::Type::kBrave); + bg_prefs.SetSelectedValue(""); + bg_prefs.SetShouldUseRandomValue(true); + } else { + bg_prefs.SetSelectedValue(custom_images.front()); + } + } + + std::move(callback).Run(); +} + +void NewTabPageHandler::OnUpdate(UpdateObserver::Source update_source) { + if (!page_.is_bound()) { + return; + } + switch (update_source) { + case UpdateObserver::Source::kBackgroundPrefs: + page_->OnBackgroundPrefsUpdated(); + break; + } +} + +} // namespace brave_new_tab diff --git a/browser/ui/webui/brave_new_tab/new_tab_page_handler.h b/browser/ui/webui/brave_new_tab/new_tab_page_handler.h new file mode 100644 index 000000000000..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..5d607f1c9fcd --- /dev/null +++ b/browser/ui/webui/brave_new_tab/new_tab_ui.cc @@ -0,0 +1,110 @@ +// 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/grit/brave_components_strings.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 { + +static constexpr webui::LocalizedString kStrings[] = { + {"backgroundSettingsTitle", IDS_NEW_TAB_BACKGROUND_SETTINGS_TITLE}, + {"braveBackgroundLabel", IDS_NEW_TAB_BRAVE_BACKGROUND_LABEL}, + {"customBackgroundLabel", IDS_NEW_TAB_CUSTOM_BACKGROUND_LABEL}, + {"customBackgroundTitle", IDS_NEW_TAB_CUSTOM_BACKGROUND_LABEL}, + {"gradientBackgroundLabel", IDS_NEW_TAB_GRADIENT_BACKGROUND_LABEL}, + {"gradientBackgroundTitle", IDS_NEW_TAB_GRADIENT_BACKGROUND_LABEL}, + {"photoCreditsText", IDS_NEW_TAB_PHOTO_CREDITS_TEXT}, + {"randomizeBackgroundLabel", IDS_NEW_TAB_RANDOMIZE_BACKGROUND_LABEL}, + {"settingsTitle", IDS_NEW_TAB_SETTINGS_TITLE}, + {"showBackgroundsLabel", IDS_NEW_TAB_SHOW_BACKGROUNDS_LABEL}, + {"showSponsoredImagesLabel", IDS_NEW_TAB_SHOW_SPONSORED_IMAGES_LABEL}, + {"solidBackgroundLabel", IDS_NEW_TAB_SOLID_BACKGROUND_LABEL}, + {"solidBackgroundTitle", IDS_NEW_TAB_SOLID_BACKGROUND_LABEL}, + {"uploadBackgroundLabel", IDS_NEW_TAB_UPLOAD_BACKGROUND_LABEL}, +}; + +// Adds support for displaying images stored in the custom background image +// folder. +void AddCustomImageDataSource(Profile* profile) { + auto* custom_background_service = + BraveNTPCustomBackgroundServiceFactory::GetForContext(profile); + if (!custom_background_service) { + return; + } + auto source = std::make_unique( + custom_background_service); + content::URLDataSource::Add(profile, std::move(source)); +} + +} // namespace + +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); + + source->OverrideContentSecurityPolicy( + network::mojom::CSPDirectiveName::ImgSrc, + "img-src chrome://resources chrome://theme chrome://background-wallpaper " + "chrome://custom-wallpaper chrome://branded-wallpaper chrome://favicon2 " + "blob: data: 'self';"); + + AddBackgroundColorToSource(source, web_ui->GetWebContents()); + AddCustomImageDataSource(profile); + + web_ui->OverrideTitle( + brave_l10n::GetLocalizedResourceUTF16String(IDS_NEW_TAB_TITLE)); + + source->AddLocalizedStrings(kStrings); +} + +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..47ffab47cb2f --- /dev/null +++ b/browser/ui/webui/brave_new_tab/new_tab_ui.h @@ -0,0 +1,39 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_BROWSER_UI_WEBUI_BRAVE_NEW_TAB_NEW_TAB_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 "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(); +}; + +} // 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 6db0b56d592d..1bbc17bb4a9c 100644 --- a/browser/ui/webui/brave_web_ui_controller_factory.cc +++ b/browser/ui/webui/brave_web_ui_controller_factory.cc @@ -40,10 +40,12 @@ #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" #include "brave/browser/ui/webui/welcome_page/brave_welcome_ui.h" +#include "brave/components/brave_new_tab/common/features.h" #include "brave/components/brave_news/common/features.h" #include "brave/components/brave_wallet/browser/brave_wallet_utils.h" #include "brave/components/brave_wallet/common/brave_wallet.mojom.h" @@ -144,6 +146,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 932463cf13a9..99c5bdf45526 100644 --- a/browser/ui/webui/new_tab_page/brave_new_tab_ui.cc +++ b/browser/ui/webui/new_tab_page/brave_new_tab_ui.cc @@ -20,7 +20,7 @@ #include "brave/browser/ui/webui/new_tab_page/brave_new_tab_message_handler.h" #include "brave/browser/ui/webui/new_tab_page/brave_new_tab_page_handler.h" #include "brave/browser/ui/webui/new_tab_page/top_sites_message_handler.h" -#include "brave/components/brave_new_tab/resources/grit/brave_new_tab_generated_map.h" +#include "brave/components/brave_new_tab_ui/grit/brave_new_tab_ui_generated_map.h" #include "brave/components/brave_news/browser/brave_news_controller.h" #include "brave/components/brave_news/common/features.h" #include "brave/components/l10n/common/localization_util.h" @@ -74,8 +74,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..7a1e52ff62f2 --- /dev/null +++ b/components/brave_new_tab/common/new_tab_page.mojom @@ -0,0 +1,91 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +module brave_new_tab.mojom; + +struct BraveBackground { + string author; + string image_url; + string link; +}; + +enum SelectedBackgroundType { + kBrave, + kCustom, + kSolid, + kGradient, +}; + +struct SelectedBackground { + SelectedBackgroundType type; + string value; +}; + +struct SponsoredImageLogo { + string alt; + string destination_url; + string image_url; +}; + +struct SponsoredImageBackground { + string wallpaper_id; + string creative_instance_id; + string campaign_id; + string image_url; + SponsoredImageLogo? logo; +}; + +// WebUI-side handler for notifications from the browser. +interface NewTabPage { + + // Called when a background-related profile preference has been updated. + OnBackgroundPrefsUpdated(); + +}; + +// Browser-side handler for requests from the WebUI page. +interface NewTabPageHandler { + + // Sets the NewTabPage remove interface that will receive notifications from + // the browser. + SetNewTabPage(pending_remote page); + + // Gets or sets whether the user has enabled background images or colors on + // the new tab page. + GetBackgroundsEnabled() => (bool enabled); + SetBackgroundsEnabled(bool enabled) => (); + + // 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..013cb8d4114b --- /dev/null +++ b/components/brave_new_tab/resources/components/app.style.ts @@ -0,0 +1,94 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import { color, font } from '@brave/leo/tokens/css/variables' +import { scoped, global } from '../lib/scoped_css' + +export const style = scoped.css` + + .settings { + --leo-icon-size: 20px; + + position: absolute; + inset-block-start: 4px; + inset-inline-end: 4px; + block-size: 20px; + inline-size: 20px; + opacity: 0.5; + color: #fff; + filter: drop-shadow(0px 1px 4px rgba(0, 0, 0, 0.60)); + + &:hover { + opacity: 0.7; + cursor: pointer; + } + } + + main { + display: flex; + flex-direction: column; + align-items: center; + min-height: 100vh; + padding-top: 40px; + } + + .topsites-container { + min-height: 32px; + } + + .searchbox-container { + flex: 1 1 auto; + margin: 16px 0; + } + + .background-caption-container { + margin: 8px 0; + } + + .widget-container { + min-height: 8px; + } + +` + +global('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..d34c297770a0 --- /dev/null +++ b/components/brave_new_tab/resources/components/app.tsx @@ -0,0 +1,45 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import * as React from 'react' +import Icon from '@brave/leo/react/icon' + +import { Background } from './background' +import { BackgroundCaption } from './background_caption' +import { SettingsDialog, SettingsView } from './settings/settings_dialog' + +import { style } from './app.style' + +export function App() { + const [settingsView, setSettingsView] = + React.useState(null) + + return ( +
+ +
+
+
+
+ +
+
+
+ + { + settingsView && + setSettingsView(null)} + /> + } +
+ ) +} diff --git a/components/brave_new_tab/resources/components/background.style.ts b/components/brave_new_tab/resources/components/background.style.ts new file mode 100644 index 000000000000..b09f26cfbe03 --- /dev/null +++ b/components/brave_new_tab/resources/components/background.style.ts @@ -0,0 +1,47 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import { scoped } from '../lib/scoped_css' + +export const style = scoped.css` + + & { + pointer-events: none; + position: fixed; + inset: 0; + z-index: -1; + display: flex; + animation-name: fade-in; + animation-timing-function: ease-in-out; + animation-duration: 350ms; + animation-delay: 0s; + animation-fill-mode: both; + + > div { + flex: 1 1 auto; + background-size: cover; + background-repeat: no-repeat; + background-position: center center; + } + } + + .image-background { + background: + linear-gradient( + rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0) 35%, rgba(0, 0, 0, 0) 80%, + rgba(0, 0, 0, 0.6) 100%), + var(--ntp-background); + } + + .color-background { + background: var(--ntp-background); + } + + @keyframes fade-in { + from { opacity: 0; } + to { opacity: 1; } + } + +` diff --git a/components/brave_new_tab/resources/components/background.tsx b/components/brave_new_tab/resources/components/background.tsx new file mode 100644 index 000000000000..3aabbe3ab68d --- /dev/null +++ b/components/brave_new_tab/resources/components/background.tsx @@ -0,0 +1,71 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import * as React from 'react' + +import { useNewTabState } from './new_tab_context' +import { useCallbackWrapper } from '../lib/use_callback_wrapper' +import { loadImage } from '../lib/image_loader' + +import { style } from './background.style' + +function setBackgroundVariable(value: string) { + if (value) { + document.body.style.setProperty('--ntp-background', value) + } else { + document.body.style.removeProperty('--ntp-background') + } +} + +function ImageBackground(props: { url: string }) { + const wrapCallback = useCallbackWrapper() + + // In order to avoid a "flash-of-unloaded-image", load the image in the + // background and only update the background CSS variable when the image has + // finished loading. + React.useEffect(() => { + loadImage(props.url).then(wrapCallback((loaded) => { + if (loaded) { + setBackgroundVariable(`url(${CSS.escape(props.url)})`) + } + })) + }, [props.url]) + + return
+} + +function ColorBackground(props: { colorValue: string }) { + React.useEffect(() => { + setBackgroundVariable(props.colorValue) + }, [props.colorValue]) + + return
+} + +export function Background() { + const currentBackground = useNewTabState((state) => state.currentBackground) + + function renderBackground() { + if (!currentBackground) { + return + } + + switch (currentBackground.type) { + case 'brave': + case 'custom': + case 'sponsored': + return + case 'solid': + case 'gradient': + return + } + } + + return ( +
+ {renderBackground()} +
+ ) +} diff --git a/components/brave_new_tab/resources/components/background_caption.style.ts b/components/brave_new_tab/resources/components/background_caption.style.ts new file mode 100644 index 000000000000..a42a3c666f3e --- /dev/null +++ b/components/brave_new_tab/resources/components/background_caption.style.ts @@ -0,0 +1,47 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import { color, font } from '@brave/leo/tokens/css/variables' +import { scoped } from '../lib/scoped_css' + +export const style = scoped.css` + a { + text-decoration: none; + color: inherit; + } + + .photo-credits { + color: ${color.white}; + font: ${font.xSmall.regular}; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.10); + opacity: .5; + } + + .sponsored-logo { + --leo-icon-size: 20px; + + display: flex; + flex-direction: column; + align-items: end; + color: ${color.white}; + + leo-icon { + opacity: 0; + transition: opacity 200ms; + } + + img { + margin: 2px 20px 0 20px; + width: 170px; + height: auto; + } + + &:hover { + leo-icon { + opacity: .7; + } + } + } +` diff --git a/components/brave_new_tab/resources/components/background_caption.tsx b/components/brave_new_tab/resources/components/background_caption.tsx new file mode 100644 index 000000000000..e627d9f024e7 --- /dev/null +++ b/components/brave_new_tab/resources/components/background_caption.tsx @@ -0,0 +1,78 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import * as React from 'react' +import Icon from '@brave/leo/react/icon' + +import formatMessage from '$web-common/formatMessage' +import { useLocale } from './locale_context' +import { useNewTabState } from './new_tab_context' + +import { + BraveBackground, + SponsoredImageBackground } from '../models/new_tab_model' + +import { style } from './background_caption.style' + +function BraveBackgroundCredits( + props: { background: BraveBackground} +) { + const { getString } = useLocale() + const { author, link } = props.background + if (!author) { + return null + } + return ( + + {formatMessage(getString('photoCreditsText'), [author])} + + ) +} + +function SponsoredBackgroundLogo( + props: { background: SponsoredImageBackground } +) { + if (!props.background.logo) { + return null + } + const { logo } = props.background + return ( + + + {logo.alt} + + ) +} + +export function BackgroundCaption() { + const currentBackground = useNewTabState((state) => state.currentBackground) + + function renderCaption() { + switch (currentBackground?.type) { + case 'brave': + return + case 'sponsored': + return + default: + return null + } + } + + return ( +
+ {renderCaption()} +
+ ) +} diff --git a/components/brave_new_tab/resources/components/locale_context.tsx b/components/brave_new_tab/resources/components/locale_context.tsx new file mode 100644 index 000000000000..dfc4ac68da7b --- /dev/null +++ b/components/brave_new_tab/resources/components/locale_context.tsx @@ -0,0 +1,59 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import * as React from 'react' + +import { StringKey } from '../lib/locale_strings' + +export interface Locale { + getString: (key: StringKey) => string + getPluralString: (key: StringKey, count: number) => Promise +} + +const Context = React.createContext({ + getString: () => '', + getPluralString: async () => '' +}) + +interface Props { + locale: Locale + children: React.ReactNode +} + +export function LocaleContext(props: Props) { + return ( + + {props.children} + + ) +} + +export function useLocale(): Locale { + return React.useContext(Context) +} + +export function usePluralString( + key: StringKey, + count: number | undefined | null +) { + const locale = useLocale() + const [value, setValue] = React.useState('') + + React.useEffect(() => { + if (typeof count !== 'number') { + setValue('') + return + } + let canUpdate = true + locale.getPluralString(key, count).then((newValue) => { + if (canUpdate) { + setValue(newValue) + } + }) + return () => { canUpdate = false } + }, [locale, count]) + + return value +} diff --git a/components/brave_new_tab/resources/components/new_tab_context.tsx b/components/brave_new_tab/resources/components/new_tab_context.tsx new file mode 100644 index 000000000000..a183899c42da --- /dev/null +++ b/components/brave_new_tab/resources/components/new_tab_context.tsx @@ -0,0 +1,32 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import * as React from 'react' + +import { NewTabModel, NewTabState, defaultModel } from '../models/new_tab_model' +import { useModelState } from '../lib/use_model_state' + +const Context = React.createContext(defaultModel()) + +interface Props { + model: NewTabModel + children: React.ReactNode +} + +export function NewTabContext(props: Props) { + return ( + + {props.children} + + ) +} + +export function useNewTabModel(): NewTabModel { + return React.useContext(Context) +} + +export function useNewTabState(map: (state: NewTabState) => T): T { + return useModelState(useNewTabModel(), map) +} diff --git a/components/brave_new_tab/resources/components/settings/background_panel.style.ts b/components/brave_new_tab/resources/components/settings/background_panel.style.ts new file mode 100644 index 000000000000..80c7b1b9d761 --- /dev/null +++ b/components/brave_new_tab/resources/components/settings/background_panel.style.ts @@ -0,0 +1,113 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import { color, font } from '@brave/leo/tokens/css/variables' +import { scoped } from '../../lib/scoped_css' + +export const style = scoped.css` + & { + display: flex; + flex-direction: column; + gap: 16px; + } + + .toggle-row { + display: flex; + align-items: center; + + label { + flex: 1 1 auto; + } + } + + .background-options { + display: flex; + flex-wrap: wrap; + gap: 16px; + + button { + display: flex; + flex-direction: column; + gap: 8px; + } + } + + .preview { + background: var(--preview-background, ${color.container.highlight}); + background-size: cover; + background-repeat: no-repeat; + background-position: center center; + border-radius: 10px; + width: 198px; + height: 156px; + } + + .background-option { + position: relative; + + &:hover .remove-image { + visibility: visible; + } + } + + .selected-marker { + --leo-icon-color: #fff; + --leo-icon-size: 24px; + + position: absolute; + inset-block-start: 10px; + inset-inline-end: 10px; + background: ${color.icon.interactive}; + border-radius: 50%; + padding: 6px; + + + .allow-remove:hover & { + visibility: hidden; + } + } + + .remove-image { + --leo-icon-size: 24px; + + position: absolute; + inset-block-start: 10px; + inset-inline-end: 10px; + background-color: #fff; + border-radius: 50%; + box-shadow: rgba(0, 0, 0, 0.5) 0px 0px 5px; + padding: 6px; + visibility: hidden; + + &:hover { + color: ${color.icon.interactive}; + } + } + + .upload { + --leo-icon-size: 36px; + --leo-progressring-size: 36px; + + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 16px; + border: solid 2px ${color.divider.subtle}; + font: ${font.small.regular}; + } + + h4 button { + --leo-icon-size: 20px; + + display: flex; + align-items: center; + gap: 4px; + + &:hover { + color: ${color.text.interactive}; + } + } +` diff --git a/components/brave_new_tab/resources/components/settings/background_panel.tsx b/components/brave_new_tab/resources/components/settings/background_panel.tsx new file mode 100644 index 000000000000..4aa6e5dd1500 --- /dev/null +++ b/components/brave_new_tab/resources/components/settings/background_panel.tsx @@ -0,0 +1,193 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import * as React from 'react' +import Icon from '@brave/leo/react/icon' +import ProgressRing from '@brave/leo/react/progressRing' +import Toggle from '@brave/leo/react/toggle' + +import { BackgroundType } from '../../models/new_tab_model' +import { useNewTabModel, useNewTabState } from '../new_tab_context' +import { useLocale } from '../locale_context' +import { useCallbackWrapper } from '../../lib/use_callback_wrapper' +import { inlineCSSVars } from '../../lib/inline_css_vars' +import { BackgroundTypePanel } from './background_type_panel' + +import { + backgroundCSSValue, + gradientPreviewBackground, + solidPreviewBackground } from '../../models/backgrounds' + +import { style } from './background_panel.style' + +export function BackgroundPanel() { + const { getString } = useLocale() + const model = useNewTabModel() + const wrapCallback = useCallbackWrapper() + + const [ + backgroundsEnabled, + sponsoredImagesEnabled, + selectedBackgroundType, + selectedBackground, + braveBackgrounds, + customBackgrounds + ] = useNewTabState((state) => [ + state.backgroundsEnabled, + state.sponsoredImagesEnabled, + state.selectedBackgroundType, + state.selectedBackground, + state.braveBackgrounds, + state.customBackgrounds + ]) + + const [panelType, setPanelType] = React.useState('none') + const [uploading, setUploading] = React.useState(false) + + function getTypePreviewValue(type: BackgroundType) { + const isSelectedType = type === selectedBackgroundType + switch (type) { + case 'brave': + return braveBackgrounds.at(0)?.imageUrl || '' + case 'custom': + if (isSelectedType && selectedBackground) { + return selectedBackground + } + return customBackgrounds.at(0) ?? '' + case 'solid': + if (isSelectedType && selectedBackground) { + return selectedBackground + } + return solidPreviewBackground + case 'gradient': + if (isSelectedType && selectedBackground) { + return selectedBackground + } + return gradientPreviewBackground + case 'none': + return '' + } + } + + function renderUploadPreview() { + return ( +
+ {uploading ? : } + {getString('uploadBackgroundLabel')} +
+ ) + } + + function renderTypePreview(type: BackgroundType) { + if (type === 'custom' && customBackgrounds.length === 0) { + return renderUploadPreview() + } + return ( +
+ { + type === selectedBackgroundType && + + + + } +
+ ) + } + + function showCustomBackgroundChooser() { + model.showCustomBackgroundChooser().then(wrapCallback((selected) => { + if (selected && !uploading) { + setUploading(true) + model.addCustomBackgrounds().then(wrapCallback(() => { + setUploading(false) + })) + } + })) + } + + function onCustomPreviewClick() { + if (customBackgrounds.length === 0) { + showCustomBackgroundChooser() + } else { + setPanelType('custom') + } + } + + if (panelType !== 'none') { + return ( +
+ ( + + )} + onClose={() => { setPanelType('none') }} + /> +
+ ) + } + + return ( +
+
+ + { model.setBackgroundsEnabled(checked) }} + /> +
+ { + backgroundsEnabled && <> +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + { + model.setSponsoredImagesEnabled(checked) + }} + /> +
+ + } +
+ ) +} diff --git a/components/brave_new_tab/resources/components/settings/background_type_panel.tsx b/components/brave_new_tab/resources/components/settings/background_type_panel.tsx new file mode 100644 index 000000000000..8032315db99d --- /dev/null +++ b/components/brave_new_tab/resources/components/settings/background_type_panel.tsx @@ -0,0 +1,146 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import * as React from 'react' +import Icon from '@brave/leo/react/icon' +import Toggle from '@brave/leo/react/toggle' + +import { BackgroundType } from '../../models/new_tab_model' +import { useNewTabModel, useNewTabState } from '../new_tab_context' +import { useLocale } from '../locale_context' +import { inlineCSSVars } from '../../lib/inline_css_vars' + +import { + backgroundCSSValue, + solidBackgrounds, + gradientBackgrounds } from '../../models/backgrounds' + +interface Props { + backgroundType: BackgroundType + isUploading: boolean + renderUploadOption: () => React.ReactNode + onClose: () => void +} + +export function BackgroundTypePanel(props: Props) { + const { getString } = useLocale() + const model = useNewTabModel() + + const [ + selectedBackgroundType, + selectedBackground, + customBackgrounds, + currentBackground + ] = useNewTabState((state) => [ + state.selectedBackgroundType, + state.selectedBackground, + state.customBackgrounds, + state.currentBackground + ]) + + const type = props.backgroundType + + function panelTitle() { + switch (props.backgroundType) { + case 'custom': return getString('customBackgroundTitle') + case 'gradient': return getString('gradientBackgroundTitle') + case 'solid': return getString('solidBackgroundTitle') + default: return '' + } + } + + function panelValues() { + switch (props.backgroundType) { + case 'custom': return customBackgrounds + case 'gradient': return gradientBackgrounds + case 'solid': return solidBackgrounds + default: return [] + } + } + + function onRandomizeToggle(detail: { checked: boolean }) { + if (detail.checked) { + model.selectBackground(type, '') + } else if (currentBackground) { + switch (currentBackground.type) { + case 'custom': + model.selectBackground(type, currentBackground.imageUrl) + break + case 'solid': + case 'gradient': + model.selectBackground(type, currentBackground.cssValue) + break + default: + break + } + } + } + + function renderOption(value: string) { + const isSelected = + selectedBackgroundType === type && + selectedBackground === value + + const classNames = ['background-option'] + if (type === 'custom') { + classNames.push('can-remove') + } + + return ( +
+ + { + type === 'custom' && + + } +
+ ) + } + + const values = panelValues() + + return ( + <> +

+ +

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

+ {getString('settingsTitle')} +

+
+ +
+ {renderPanel()} +
+
+
+
+ ) +} diff --git a/components/brave_new_tab/resources/lib/image_loader.ts b/components/brave_new_tab/resources/lib/image_loader.ts new file mode 100644 index 000000000000..a406d2b56808 --- /dev/null +++ b/components/brave_new_tab/resources/lib/image_loader.ts @@ -0,0 +1,38 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +export const placeholderImageSrc = + 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg"%3E%3C/svg%3E' + +// Loads an image in the background and resolves when the image has either +// loaded or was unable to load. +export function loadImage(url: string): Promise { + return new Promise((resolve) => { + if (!url) { + resolve(false) + return + } + + const unlisten = () => { + image.removeEventListener('load', onLoad) + image.removeEventListener('error', onError) + } + + const onLoad = () => { + unlisten() + resolve(true) + } + + const onError = () => { + unlisten() + resolve(false) + } + + const image = new Image() + image.addEventListener('load', onLoad) + image.addEventListener('error', onError) + image.src = url + }) +} diff --git a/components/brave_new_tab/resources/lib/inline_css_vars.ts b/components/brave_new_tab/resources/lib/inline_css_vars.ts new file mode 100644 index 000000000000..20b14727477b --- /dev/null +++ b/components/brave_new_tab/resources/lib/inline_css_vars.ts @@ -0,0 +1,14 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +interface CSSVars { + [key: `--${string}`]: string | number +} + +// Allows a collection of CSS custom variables to be used in the "style" prop of +// React components. +export function inlineCSSVars(vars: CSSVars) { + return vars as React.CSSProperties +} diff --git a/components/brave_new_tab/resources/lib/locale_strings.ts b/components/brave_new_tab/resources/lib/locale_strings.ts new file mode 100644 index 000000000000..2e7c6ddbd8bb --- /dev/null +++ b/components/brave_new_tab/resources/lib/locale_strings.ts @@ -0,0 +1,20 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +export type StringKey = + 'backgroundSettingsTitle' | + 'braveBackgroundLabel' | + 'customBackgroundLabel' | + 'customBackgroundTitle' | + 'gradientBackgroundLabel' | + 'gradientBackgroundTitle' | + 'photoCreditsText' | + 'randomizeBackgroundLabel' | + 'settingsTitle' | + 'showBackgroundsLabel' | + 'showSponsoredImagesLabel' | + 'solidBackgroundLabel' | + 'solidBackgroundTitle' | + 'uploadBackgroundLabel' diff --git a/components/brave_new_tab/resources/lib/scoped_css.ts b/components/brave_new_tab/resources/lib/scoped_css.ts new file mode 100644 index 000000000000..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/store.ts b/components/brave_new_tab/resources/lib/store.ts new file mode 100644 index 000000000000..ade0b05ab71b --- /dev/null +++ b/components/brave_new_tab/resources/lib/store.ts @@ -0,0 +1,80 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +type Listener = (state: State) => void + +type UpdateFunction = (state: State) => Partial + +// A simple object-state store. +export interface Store { + + // Returns the current state of the store. + getState: () => State + + // Updates the state of the store. All listeners will be notified of the state + // change. + update: (source: Partial | UpdateFunction) => void + + // Adds a listener that will be notified when the state store changes. The + // listener will not be notifified immediately. Returns a function that will + // remove the listener from store. + addListener: (listener: Listener) => (() => void) + +} + +export function createStore(initialState: State): Store { + const listeners = new Set>() + const state = { ...initialState } + let notificationQueued = false + + function notify() { + if (notificationQueued) { + return + } + notificationQueued = true + queueMicrotask(() => { + notificationQueued = false + for (const listener of listeners) { + if (notificationQueued) { + break + } + try { + listener(state) + } catch (e) { + queueMicrotask(() => { throw e }) + } + } + }) + } + + return { + + getState() { + return state + }, + + update(source: Partial | UpdateFunction) { + if (typeof source === 'function') { + source = source(state) + } + for (const [key, value] of Object.entries(source)) { + if (value !== undefined) { + (state as any)[key] = value + } + } + notify() + }, + + addListener(listener: Listener) { + if (!listeners.has(listener)) { + listeners.add(listener) + } + return () => { + listeners.delete(listener) + } + } + + } +} diff --git a/components/brave_new_tab/resources/lib/use_callback_wrapper.ts b/components/brave_new_tab/resources/lib/use_callback_wrapper.ts new file mode 100644 index 000000000000..f0e91b5e6b91 --- /dev/null +++ b/components/brave_new_tab/resources/lib/use_callback_wrapper.ts @@ -0,0 +1,32 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import * as React from 'react' + +// A hook useful for integrating async APIs with component state. +// +// When setting up a promise callback from a component event handler, it might +// turn out that the component is dismounted before the promise resolves. In +// such a case, you may see the following warning: +// +// "Can't perform a React state update on an unmounted component." +// +// By wrapping callbacks with the function returned by `useCallbackWrapper`, the +// wrapped callback will not be executed after the component has dismounted. +export function useCallbackWrapper() { + const active = React.useRef(true) + React.useEffect(() => { + return () => { active.current = false } + }, []) + return (callback: (...args: [...T]) => R) => { + return (...args: [...T]) => { + if (active.current) { + return callback(...args) + } else { + return undefined + } + } + } +} diff --git a/components/brave_new_tab/resources/lib/use_model_state.ts b/components/brave_new_tab/resources/lib/use_model_state.ts new file mode 100644 index 000000000000..1d14a8651c30 --- /dev/null +++ b/components/brave_new_tab/resources/lib/use_model_state.ts @@ -0,0 +1,35 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import * as React from 'react' + +type StateListener = (state: State) => void + +interface Model { + getState: () => State + addListener: (listener: StateListener) => () => void +} + +// A helper for importing model state into component state. This helper can be +// used by model context modules when implementing "useFooState" hooks. +export function useModelState( + model: Model, + map: (state: State) => T +): T { + const [value, setValue] = React.useState(() => map(model.getState())) + React.useEffect(() => { + return model.addListener((state) => { + const result = map(state) + if (result === value) { + // If `map` is the identity function, then call `setState` with a new + // object in order to ensure a re-render. + setValue({ ...result }) + } else { + setValue(result) + } + }) + }, [model]) + return value +} diff --git a/components/brave_new_tab/resources/models/backgrounds.ts b/components/brave_new_tab/resources/models/backgrounds.ts new file mode 100644 index 000000000000..a0f3045ef47b --- /dev/null +++ b/components/brave_new_tab/resources/models/backgrounds.ts @@ -0,0 +1,90 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import { NewTabState, Background, BackgroundType } from './new_tab_model' + +export const solidBackgrounds = [ + '#5B5C63', '#000000', '#151E9A', '#2197F9', '#1FC3DC', '#086582', '#67D4B4', + '#077D5A', '#3C790B', '#AFCE57', '#F0CB44', '#F28A29', '#FC798F', '#C1226E', + '#FAB5EE', '#C0C4FF', '#9677EE', '#5433B0', '#4A000C' +] + +export const solidPreviewBackground = solidBackgrounds[2] + +export const gradientBackgrounds = [ + 'linear-gradient(125.83deg, #392DD1 0%, #A91B78 99.09%)', + 'linear-gradient(125.83deg, #392DD1 0%, #22B8CF 99.09%)', + 'linear-gradient(90deg, #4F30AB 0.64%, #845EF7 99.36%)', + 'linear-gradient(126.47deg, #A43CE4 16.99%, #A72B6D 86.15%)', + 'radial-gradient(' + + '69.45% 69.45% at 89.46% 81.73%, #641E0C 0%, #500F39 43.54%, #060141 100%)', + 'radial-gradient(80% 80% at 101.61% 76.99%, #2D0264 0%, #030023 100%)', + 'linear-gradient(128.12deg, #43D4D4 6.66%, #1596A9 83.35%)', + 'linear-gradient(323.02deg, #DD7131 18.65%, #FBD460 82.73%)', + 'linear-gradient(128.12deg, #4F86E2 6.66%, #694CD9 83.35%)', + 'linear-gradient(127.39deg, #851B6A 6.04%, #C83553 86.97%)', + 'linear-gradient(130.39deg, #FE6F4C 9.83%, #C53646 85.25%)' +] + +export const gradientPreviewBackground = gradientBackgrounds[0] + +const defaultBackground: Background = { + type: 'gradient', + cssValue: gradientPreviewBackground +} + +function chooseRandom(list: T[]): T | null { + const index = Math.floor(Math.random() * list.length) + return list.at(index) || null +} + +export function getCurrentBackground(state: NewTabState): Background | null { + const { + backgroundsEnabled, + braveBackgrounds, + customBackgrounds, + selectedBackground, + selectedBackgroundType, + sponsoredImageBackground } = 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 + } + } +} + +export function backgroundCSSValue(type: BackgroundType, value: string) { + switch (type) { + case 'brave': + case 'custom': return `url(${CSS.escape(value)})` + case 'solid': + case 'gradient': + case 'none': return value + } +} diff --git a/components/brave_new_tab/resources/models/new_tab_model.ts b/components/brave_new_tab/resources/models/new_tab_model.ts new file mode 100644 index 000000000000..ca0a0aca8af0 --- /dev/null +++ b/components/brave_new_tab/resources/models/new_tab_model.ts @@ -0,0 +1,92 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +export type BackgroundType = 'brave' | 'custom' | 'solid' | 'gradient' | 'none' + +export interface BraveBackground { + type: 'brave' + author: string + imageUrl: string + link: string +} + +export interface ColorBackground { + type: 'solid' | 'gradient' + cssValue: string +} + +export interface CustomBackground { + type: 'custom' + imageUrl: string +} + +export interface SponsoredImageLogo { + alt: string + destinationUrl: string + imageUrl: string +} + +export interface SponsoredImageBackground { + type: 'sponsored' + imageUrl: string, + creativeInstanceId: string + wallpaperId: string + logo: SponsoredImageLogo | undefined +} + +export type Background = + BraveBackground | + ColorBackground | + CustomBackground | + SponsoredImageBackground + +export interface NewTabState { + backgroundsEnabled: boolean + 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 + setBackgroundsEnabled: (enabled: boolean) => void + setSponsoredImagesEnabled: (enabled: boolean) => void + selectBackground: (type: BackgroundType, value: string) => void + showCustomBackgroundChooser: () => Promise + addCustomBackgrounds: () => Promise + removeCustomBackground: (background: string) => void +} + +export function defaultModel(): NewTabModel { + const state = defaultState() + return { + getState() { return state }, + addListener() { return () => {} }, + setBackgroundsEnabled(enabled) {}, + setSponsoredImagesEnabled(enabled) {}, + selectBackground(type, value) {}, + async showCustomBackgroundChooser() { return false }, + async addCustomBackgrounds() {}, + removeCustomBackground(background) {} + } +} 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..bc6d49b2f56e --- /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 './components/locale_context' +import { NewTabContext } from './components/new_tab_context' +import { createNewTabModel } from './webui/webui_new_tab_model' +import { createLocale } from './webui/webui_locale' +import { App } from './components/app' + +setIconBasePath('chrome://resources/brave-icons') + +function whenDocumentReady() { + return new Promise((resolve) => { + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => resolve()) + } else { + resolve() + } + }) +} + +whenDocumentReady().then(() => { + const newTabModel = createNewTabModel() + + Object.assign(self, { + [Symbol.for('newTabModel')]: newTabModel + }) + + createRoot(document.getElementById('root')!).render( + + + + + + ) +}) diff --git a/components/brave_new_tab/resources/stories/index.tsx b/components/brave_new_tab/resources/stories/index.tsx new file mode 100644 index 000000000000..ade09044f1f4 --- /dev/null +++ b/components/brave_new_tab/resources/stories/index.tsx @@ -0,0 +1,29 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import * as React from 'react' + +import { LocaleContext } from '../components/locale_context' +import { NewTabContext } from '../components/new_tab_context' +import { createNewTabModel } from './sb_new_tab_model' +import { createLocale } from './sb_locale' +import { App } from '../components/app' + +export default { + title: 'New Tab/Next' +} + +export function NTPNext() { + const newTabModel = React.useMemo(() => createNewTabModel(), []) + return ( + + +
+ +
+
+
+ ) +} diff --git a/components/brave_new_tab/resources/stories/sb_locale.ts b/components/brave_new_tab/resources/stories/sb_locale.ts new file mode 100644 index 000000000000..3323aeb07e29 --- /dev/null +++ b/components/brave_new_tab/resources/stories/sb_locale.ts @@ -0,0 +1,34 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import { StringKey } from '../lib/locale_strings' + +const localeStrings: { [K in StringKey]: string } = { + backgroundSettingsTitle: 'Background Image', + braveBackgroundLabel: 'Brave backgrounds', + customBackgroundLabel: 'Use your own', + customBackgroundTitle: 'Use your own', + gradientBackgroundLabel: 'Gradients', + gradientBackgroundTitle: 'Gradients', + photoCreditsText: 'Photo by $1', + randomizeBackgroundLabel: 'Refresh on every new tab', + settingsTitle: 'Customize Dashboard', + showBackgroundsLabel: 'Show background images', + showSponsoredImagesLabel: 'Show Sponsored Images', + solidBackgroundLabel: 'Solid colors', + solidBackgroundTitle: 'Solid colors', + uploadBackgroundLabel: 'Upload from device' +} + +export function createLocale() { + const getString = + (key: string) => String((localeStrings as any)[key] || 'MISSING') + return { + getString, + async getPluralString (key: string, count: number) { + return getString(key).replace('#', String(count)) + } + } +} diff --git a/components/brave_new_tab/resources/stories/sb_new_tab_model.ts b/components/brave_new_tab/resources/stories/sb_new_tab_model.ts new file mode 100644 index 000000000000..fde0002bb535 --- /dev/null +++ b/components/brave_new_tab/resources/stories/sb_new_tab_model.ts @@ -0,0 +1,106 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import { createStore } from '../lib/store' +import { getCurrentBackground } from '../models/backgrounds' + +import { + NewTabModel, + SponsoredImageBackground, + defaultModel, + defaultState } from '../models/new_tab_model' + +function delay(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms) + }) +} + +const sampleBackground = + 'https://brave.com/static-assets/images/brave-logo-sans-text.svg' + +const sampleSponsoredImage: SponsoredImageBackground = { + type: 'sponsored', + imageUrl: sampleBackground, + creativeInstanceId: '', + wallpaperId: '', + logo: { + alt: 'Be Brave!', + destinationUrl: 'https://brave.com', + imageUrl: sampleBackground + } +} + +export function createNewTabModel(): NewTabModel { + const store = createStore(defaultState()) + store.update({ + braveBackgrounds: [ + { + type: 'brave', + author: 'John Doe', + imageUrl: sampleBackground, + link: 'https://brave.com' + } + ], + sponsoredImageBackground: sampleSponsoredImage && null + }) + + store.update({ + currentBackground: getCurrentBackground(store.getState()) + }) + + return { + ...defaultModel(), + + getState: store.getState, + addListener: store.addListener, + + setBackgroundsEnabled(enabled) { + store.update({ backgroundsEnabled: enabled }) + store.update({ + currentBackground: getCurrentBackground(store.getState()) + }) + }, + + setSponsoredImagesEnabled(enabled) { + store.update({ sponsoredImagesEnabled: enabled }) + }, + + selectBackground(type, value) { + store.update({ + selectedBackgroundType: type, + selectedBackground: value + }) + store.update({ + currentBackground: getCurrentBackground(store.getState()) + }) + }, + + async showCustomBackgroundChooser() { + return true + }, + + async addCustomBackgrounds() { + await delay(2000) + + store.update((state) => ({ + customBackgrounds: [...state.customBackgrounds, sampleBackground], + selectedBackground: sampleBackground, + selectedBackgroundType: 'custom' + })) + + store.update({ + currentBackground: getCurrentBackground(store.getState()) + }) + }, + + async removeCustomBackground(background) { + store.update((state) => ({ + customBackgrounds: + state.customBackgrounds.filter((elem) => elem !== background) + })) + } + } +} diff --git a/components/brave_new_tab/resources/tsconfig.json b/components/brave_new_tab/resources/tsconfig.json new file mode 100644 index 000000000000..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/callback_listeners.ts b/components/brave_new_tab/resources/webui/callback_listeners.ts new file mode 100644 index 000000000000..d40e2e0981c7 --- /dev/null +++ b/components/brave_new_tab/resources/webui/callback_listeners.ts @@ -0,0 +1,15 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +export function addCallbackListeners(router: any, listeners: Partial) { + for (const [key, value] of Object.entries(listeners)) { + router[key].addListener(value) + } + return () => { + for (const [key, value] of Object.entries(listeners)) { + router[key].removeListener(value) + } + } +} diff --git a/components/brave_new_tab/resources/webui/debouncer.ts b/components/brave_new_tab/resources/webui/debouncer.ts new file mode 100644 index 000000000000..015ae3e4726c --- /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) + }, 10) + } +} diff --git a/components/brave_new_tab/resources/webui/new_tab_page_proxy.ts b/components/brave_new_tab/resources/webui/new_tab_page_proxy.ts new file mode 100644 index 000000000000..b373059065c1 --- /dev/null +++ b/components/brave_new_tab/resources/webui/new_tab_page_proxy.ts @@ -0,0 +1,29 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import * as mojom from 'gen/brave/components/brave_new_tab/common/new_tab_page.mojom.m.js' + +let instance: NewTabPageProxy | null = null + +export class NewTabPageProxy { + callbackRouter: mojom.NewTabPageCallbackRouter + handler: mojom.NewTabPageHandlerRemote + + constructor(callbackRouter: mojom.NewTabPageCallbackRouter, + handler: mojom.NewTabPageHandlerRemote) { + this.callbackRouter = callbackRouter + this.handler = handler + } + + static getInstance(): NewTabPageProxy { + if (!instance) { + const callbackRouter = new mojom.NewTabPageCallbackRouter() + const handler = mojom.NewTabPageHandler.getRemote() + handler.setNewTabPage(callbackRouter.$.bindNewPipeAndPassRemote()) + instance = new NewTabPageProxy(callbackRouter, handler) + } + return instance + } +} diff --git a/components/brave_new_tab/resources/webui/webui_locale.ts b/components/brave_new_tab/resources/webui/webui_locale.ts new file mode 100644 index 000000000000..a7d774f5dce2 --- /dev/null +++ b/components/brave_new_tab/resources/webui/webui_locale.ts @@ -0,0 +1,18 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import { loadTimeData } from 'chrome://resources/js/load_time_data.js' + +export function createLocale() { + return { + getString(key: string) { + return loadTimeData.getString(key) + }, + + async getPluralString(key: string, count: number) { + throw new Error('Not implemented') + } + } +} diff --git a/components/brave_new_tab/resources/webui/webui_new_tab_model.ts b/components/brave_new_tab/resources/webui/webui_new_tab_model.ts new file mode 100644 index 000000000000..aa8696f0ef72 --- /dev/null +++ b/components/brave_new_tab/resources/webui/webui_new_tab_model.ts @@ -0,0 +1,151 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import * as mojom from 'gen/brave/components/brave_new_tab/common/new_tab_page.mojom.m.js' + +import { NewTabPageProxy } from './new_tab_page_proxy' +import { NewTabModel, BackgroundType, defaultState } from '../models/new_tab_model' +import { createStore } from '../lib/store' +import { getCurrentBackground } from '../models/backgrounds' +import { debounceEvent } from './debouncer' +import { addCallbackListeners } from './callback_listeners' + +export function backgroundTypeFromMojo(type: number): BackgroundType { + switch (type) { + case mojom.SelectedBackgroundType.kBrave: return 'brave' + case mojom.SelectedBackgroundType.kCustom: return 'custom' + case mojom.SelectedBackgroundType.kSolid: return 'solid' + case mojom.SelectedBackgroundType.kGradient: return 'gradient' + default: return 'none' + } +} + +export function backgroundTypeToMojo(type: BackgroundType) { + switch (type) { + case 'brave': return mojom.SelectedBackgroundType.kBrave + case 'custom': return mojom.SelectedBackgroundType.kCustom + case 'solid': return mojom.SelectedBackgroundType.kSolid + case 'gradient': return mojom.SelectedBackgroundType.kGradient + case 'none': return mojom.SelectedBackgroundType.kSolid + } +} + +export function createNewTabModel(): NewTabModel { + const { handler, callbackRouter} = NewTabPageProxy.getInstance() + const store = createStore(defaultState()) + + function updateCurrentBackground() { + store.update({ + currentBackground: getCurrentBackground(store.getState()) + }) + } + + async function updateBackgroundsEnabled() { + const { enabled } = await handler.getBackgroundsEnabled() + store.update({ backgroundsEnabled: enabled }) + } + + async function updateSponsoredImagesEnabled() { + const { enabled } = await handler.getSponsoredImagesEnabled() + store.update({ sponsoredImagesEnabled: enabled }) + } + + async function updateBraveBackgrounds() { + const { backgrounds } = await handler.getBraveBackgrounds() + store.update({ + braveBackgrounds: backgrounds.map((item) => ({ type: 'brave', ...item })) + }) + } + + async function updateSelectedBackground() { + const { background } = await handler.getSelectedBackground() + if (background) { + store.update({ + selectedBackgroundType: backgroundTypeFromMojo(background.type), + selectedBackground: background.value + }) + } else { + store.update({ + selectedBackgroundType: 'none', + selectedBackground: '' + }) + } + } + + async function updateCustomBackgrounds() { + const { backgrounds } = await handler.getCustomBackgrounds() + store.update({ customBackgrounds: backgrounds }) + } + + async function updateSponsoredImageBackground() { + const { background } = await handler.getSponsoredImageBackground() + store.update({ + sponsoredImageBackground: + background ? { type: 'sponsored', ...background } : null + }) + } + + addCallbackListeners(callbackRouter, { + onBackgroundPrefsUpdated: debounceEvent(async () => { + await Promise.all([ + updateCustomBackgrounds(), + updateSelectedBackground(), + ]) + updateCurrentBackground() + }) + }) + + async function loadData() { + await Promise.all([ + updateBackgroundsEnabled(), + updateSponsoredImagesEnabled(), + updateBraveBackgrounds(), + updateCustomBackgrounds(), + updateSelectedBackground(), + updateSponsoredImageBackground() + ]) + + updateCurrentBackground() + } + + loadData() + + return { + getState: store.getState, + + addListener: store.addListener, + + setBackgroundsEnabled(enabled) { + store.update({ backgroundsEnabled: enabled }) + handler.setBackgroundsEnabled(enabled) + }, + + setSponsoredImagesEnabled(enabled) { + store.update({ sponsoredImagesEnabled: enabled }) + handler.setSponsoredImagesEnabled(enabled) + }, + + selectBackground(type, value) { + store.update({ + selectedBackgroundType: type, + selectedBackground: value + }) + handler.selectBackground({ type: backgroundTypeToMojo(type), value }) + }, + + async showCustomBackgroundChooser() { + const { imagesSelected } = await handler.showCustomBackgroundChooser() + return imagesSelected + }, + + async addCustomBackgrounds() { + await handler.addCustomBackgrounds() + }, + + async removeCustomBackground(background) { + await handler.removeCustomBackground(background) + } + } +} diff --git a/components/brave_new_tab_ui/BUILD.gn b/components/brave_new_tab_ui/BUILD.gn index 83fd783a67df..fe6d469cf0eb 100644 --- a/components/brave_new_tab_ui/BUILD.gn +++ b/components/brave_new_tab_ui/BUILD.gn @@ -8,7 +8,7 @@ import("//mojo/public/tools/bindings/mojom.gni") transpile_web_ui("brave_new_tab_ui") { entry_points = [ [ - "brave_new_tab", + "brave_new_tab_ui", rebase_path("brave_new_tab.tsx"), ] ] public_deps = [ @@ -22,12 +22,12 @@ transpile_web_ui("brave_new_tab_ui") { "//ui/webui/resources/cr_components/searchbox:mojo_bindings_js", ] } - resource_name = "brave_new_tab" + resource_name = "brave_new_tab_ui" } pack_web_resources("generated_resources") { - resource_name = "brave_new_tab" - output_dir = "$root_gen_dir/brave/components/brave_new_tab/resources" + resource_name = "brave_new_tab_ui" + output_dir = "$root_gen_dir/brave/components/brave_new_tab_ui" deps = [ ":brave_new_tab_ui" ] } diff --git a/components/brave_new_tab_ui/brave_new_tab.html b/components/brave_new_tab_ui/brave_new_tab.html index 8701520c371a..9eafbcd91b8e 100644 --- a/components/brave_new_tab_ui/brave_new_tab.html +++ b/components/brave_new_tab_ui/brave_new_tab.html @@ -13,7 +13,7 @@ - +