diff --git a/browser/about_flags.cc b/browser/about_flags.cc index 759738fed4d..29865efff24 100644 --- a/browser/about_flags.cc +++ b/browser/about_flags.cc @@ -23,6 +23,7 @@ #include "brave/components/debounce/common/features.h" #include "brave/components/ipfs/buildflags/buildflags.h" #include "brave/components/ntp_background_images/browser/features.h" +#include "brave/components/playlist/buildflags/buildflags.h" #include "brave/components/skus/browser/skus_utils.h" #include "brave/components/skus/common/features.h" #include "brave/components/speedreader/common/buildflags.h" @@ -43,6 +44,12 @@ #include "brave/components/ipfs/features.h" #endif +#if BUILDFLAG(ENABLE_PLAYLIST) +#include "brave/components/playlist/features.h" +#include "chrome/common/channel_info.h" +#include "components/version_info/version_info.h" +#endif + using brave_shields::features::kBraveAdblockCnameUncloaking; using brave_shields::features::kBraveAdblockCollapseBlockedElements; using brave_shields::features::kBraveAdblockCookieListDefault; @@ -321,6 +328,9 @@ constexpr char kRestrictWebSocketsPoolName[] = "Restrict WebSockets pool"; constexpr char kRestrictWebSocketsPoolDescription[] = "Limits simultaneous active WebSockets connections per eTLD+1"; +constexpr char kPlaylistName[] = "Playlist"; +constexpr char kPlaylistDescription[] = "Enables Playlist"; + } // namespace } // namespace flag_descriptions @@ -449,6 +459,28 @@ constexpr char kRestrictWebSocketsPoolDescription[] = #define BRAVE_TRANSLATE_GO_FEATURE_ENTRIES #endif // BUILDFLAG(ENABLE_BRAVE_TRANSLATE_GO) +#if BUILDFLAG(ENABLE_PLAYLIST) +uint16_t AllowForDevVersion(uint16_t os) { + if (auto channel = chrome::GetChannel(); + channel == version_info::Channel::STABLE || + channel == version_info::Channel::BETA) { + return 0; + } + + return os; +} + +#define PLAYLIST_FEATURE_ENTRIES \ + {"playlist", \ + flag_descriptions::kPlaylistName, \ + flag_descriptions::kPlaylistDescription, \ + AllowForDevVersion( \ + flags_ui::kOsMac | flags_ui::kOsWin | flags_ui::kOsLinux), \ + FEATURE_VALUE_TYPE(playlist::features::kPlaylist)}, +#else +#define PLAYLIST_FEATURE_ENTRIES +#endif + #define BRAVE_ABOUT_FLAGS_FEATURE_ENTRIES \ {"use-dev-updater-url", \ flag_descriptions::kUseDevUpdaterUrlName, \ @@ -582,4 +614,5 @@ constexpr char kRestrictWebSocketsPoolDescription[] = BRAVE_SKU_SDK_FEATURE_ENTRIES \ SPEEDREADER_FEATURE_ENTRIES \ BRAVE_TRANSLATE_GO_FEATURE_ENTRIES \ - BRAVE_FEDERATED_FEATURE_ENTRIES + BRAVE_FEDERATED_FEATURE_ENTRIES \ + PLAYLIST_FEATURE_ENTRIES diff --git a/browser/brave_content_browser_client.cc b/browser/brave_content_browser_client.cc index 9e573e54091..a87ba419933 100644 --- a/browser/brave_content_browser_client.cc +++ b/browser/brave_content_browser_client.cc @@ -65,6 +65,8 @@ #include "brave/components/ftx/browser/buildflags/buildflags.h" #include "brave/components/gemini/browser/buildflags/buildflags.h" #include "brave/components/ipfs/buildflags/buildflags.h" +#include "brave/components/playlist/buildflags/buildflags.h" +#include "brave/components/playlist/features.h" #include "brave/components/sidebar/buildflags/buildflags.h" #include "brave/components/skus/common/skus_sdk.mojom.h" #include "brave/components/speedreader/common/buildflags.h" @@ -213,6 +215,10 @@ using extensions::ChromeContentBrowserClientExtensionsPart; #include "brave/browser/brave_ads/brave_ads_host.h" #endif // BUILDFLAG(IS_ANDROID) +#if BUILDFLAG(ENABLE_PLAYLIST) +#include "brave/browser/ui/webui/playlist_ui.h" +#endif // BUILDFLAG(ENABLE_PLAYLIST) + namespace { bool HandleURLReverseOverrideRewrite(GURL* url, @@ -592,6 +598,13 @@ void BraveContentBrowserClient::RegisterBrowserInterfaceBindersForFrame( } #endif +#if BUILDFLAG(ENABLE_PLAYLIST) + if (base::FeatureList::IsEnabled(playlist::features::kPlaylist)) { + chrome::internal::RegisterWebUIControllerInterfaceBinder< + playlist::mojom::PageHandlerFactory, playlist::PlaylistUI>(map); + } +#endif + #if !BUILDFLAG(IS_ANDROID) chrome::internal::RegisterWebUIControllerInterfaceBinder< brave_new_tab_page::mojom::PageHandlerFactory, BraveNewTabUI>(map); diff --git a/browser/brave_shields/brave_shields_web_contents_observer.cc b/browser/brave_shields/brave_shields_web_contents_observer.cc index f4521ddbc42..742e4a35106 100644 --- a/browser/brave_shields/brave_shields_web_contents_observer.cc +++ b/browser/brave_shields/brave_shields_web_contents_observer.cc @@ -166,7 +166,8 @@ void BraveShieldsWebContentsObserver::DispatchBlockedEventForWebContents( return; auto* shields_data_ctrlr = brave_shields::BraveShieldsDataController::FromWebContents(web_contents); - DCHECK(shields_data_ctrlr); + // |shields_data_ctrlr| can be null if the |web_contents| is generated in + // component layer - We don't attach any tab helpers in this case. if (!shields_data_ctrlr) return; shields_data_ctrlr->HandleItemBlocked(block_type, subresource); diff --git a/browser/ui/BUILD.gn b/browser/ui/BUILD.gn index 6b9c8cc7bc2..afc1b425540 100644 --- a/browser/ui/BUILD.gn +++ b/browser/ui/BUILD.gn @@ -5,6 +5,7 @@ import("//brave/components/brave_wayback_machine/buildflags/buildflags.gni") import("//brave/components/ftx/browser/buildflags/buildflags.gni") import("//brave/components/ipfs/buildflags/buildflags.gni") import("//brave/components/ntp_background_images/buildflags/buildflags.gni") +import("//brave/components/playlist/buildflags/buildflags.gni") import("//brave/components/sidebar/buildflags/buildflags.gni") import("//brave/components/speedreader/common/buildflags.gni") import("//brave/components/tor/buildflags/buildflags.gni") @@ -308,6 +309,8 @@ source_set("ui") { "//brave/components/ntp_background_images/common", "//brave/components/ntp_widget_utils/browser", "//brave/components/p3a:buildflags", + "//brave/components/playlist/buildflags", + "//brave/components/resources:static_resources", "//brave/components/sidebar/buildflags", "//brave/components/time_period_storage", "//brave/components/tor", @@ -658,6 +661,19 @@ source_set("ui") { deps += [ "//brave/components/ftx/common" ] } + if (enable_playlist) { + sources += [ + "webui/playlist_page_handler.cc", + "webui/playlist_page_handler.h", + "webui/playlist_ui.cc", + "webui/playlist_ui.h", + ] + deps += [ + "//brave/components/playlist/mojom", + "//brave/components/playlist/resources:generated_resources", + ] + } + if (toolkit_views) { deps += [ "//components/constrained_window", diff --git a/browser/ui/webui/brave_web_ui_controller_factory.cc b/browser/ui/webui/brave_web_ui_controller_factory.cc index cf47ce95418..5fac62d7b58 100644 --- a/browser/ui/webui/brave_web_ui_controller_factory.cc +++ b/browser/ui/webui/brave_web_ui_controller_factory.cc @@ -21,6 +21,7 @@ #include "brave/components/constants/pref_names.h" #include "brave/components/constants/webui_url_constants.h" #include "brave/components/ipfs/buildflags/buildflags.h" +#include "brave/components/playlist/buildflags/buildflags.h" #include "brave/components/sidebar/buildflags/buildflags.h" #include "brave/components/tor/buildflags/buildflags.h" #include "build/build_config.h" @@ -55,6 +56,10 @@ #include "brave/components/ipfs/ipfs_utils.h" #endif +#if BUILDFLAG(ENABLE_PLAYLIST) +#include "brave/browser/ui/webui/playlist_ui.h" +#endif + #if BUILDFLAG(ENABLE_TOR) #include "brave/browser/ui/webui/tor_internals_ui.h" #endif @@ -86,7 +91,7 @@ WebUIController* NewWebUI(WebUI* web_ui, const GURL& url) { } else if (host == kIPFSWebUIHost && ipfs::IpfsServiceFactory::IsIpfsEnabled(profile)) { return new IPFSUI(web_ui, url.host()); -#endif // BUILDFLAG(ENABLE_IPFS) +#endif #if !BUILDFLAG(IS_ANDROID) } else if (host == kWalletPageHost) { if (brave_wallet::IsNativeWalletEnabled()) { @@ -101,6 +106,10 @@ WebUIController* NewWebUI(WebUI* web_ui, const GURL& url) { } else if (host == kWalletPanelHost) { return new WalletPanelUI(web_ui); #endif // BUILDFLAG(OS_ANDROID) +#if BUILDFLAG(ENABLE_PLAYLIST) + } else if (host == kPlaylistHost) { + return new playlist::PlaylistUI(web_ui, url.host()); +#endif // BUILDFLAG(PLAYLIST_ENABLED) } else if (host == kRewardsPageHost) { return new BraveRewardsPageUI(web_ui, url.host()); } else if (host == kRewardsInternalsHost) { @@ -169,6 +178,9 @@ WebUIFactoryFunction GetWebUIFactoryFunction(WebUI* web_ui, const GURL& url) { #endif #if BUILDFLAG(ENABLE_TOR) url.host_piece() == kTorInternalsHost || +#endif +#if BUILDFLAG(ENABLE_PLAYLIST) + url.host_piece() == kPlaylistHost || #endif url.host_piece() == kWelcomeHost || url.host_piece() == chrome::kChromeUIWelcomeURL || @@ -213,6 +225,10 @@ WebUI::TypeID BraveWebUIControllerFactory::GetWebUIType( return WebUI::kNoWebUI; } #endif // BUILDFLAG(IS_ANDROID) +#if BUILDFLAG(ENABLE_PLAYLIST) + if (playlist::PlaylistUI::ShouldBlockPlaylistWebUI(browser_context, url)) + return WebUI::kNoWebUI; +#endif WebUIFactoryFunction function = GetWebUIFactoryFunction(NULL, url); if (function) { return reinterpret_cast(function); diff --git a/browser/ui/webui/playlist_page_handler.cc b/browser/ui/webui/playlist_page_handler.cc new file mode 100644 index 00000000000..58f70608fc0 --- /dev/null +++ b/browser/ui/webui/playlist_page_handler.cc @@ -0,0 +1,108 @@ +/* Copyright (c) 2022 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 http://mozilla.org/MPL/2.0/. */ + +#include "brave/browser/ui/webui/playlist_page_handler.h" + +#include +#include +#include + +#include "brave/browser/playlist/playlist_service_factory.h" +#include "brave/components/playlist/playlist_constants.h" +#include "chrome/browser/profiles/profile.h" + +playlist::PlaylistService* GetPlaylistService(Profile* profile) { + return playlist::PlaylistServiceFactory::GetForBrowserContext(profile); +} + +PlaylistPageHandler::PlaylistPageHandler( + Profile* profile, + mojo::PendingReceiver pending_page_handler, + mojo::PendingRemote pending_page) + : profile_(profile), + page_(std::move(pending_page)), + handler_(this, std::move(pending_page_handler)) { + DCHECK(profile_); + observation_.Observe(GetPlaylistService(profile_)); +} + +PlaylistPageHandler::~PlaylistPageHandler() = default; + +void PlaylistPageHandler::GetAllPlaylists( + PlaylistPageHandler::GetAllPlaylistsCallback callback) { + auto value = GetPlaylistService(profile_)->GetAllPlaylistItems(); + DCHECK(value.is_list()); + + std::vector> playlists; + playlists.push_back(playlist::mojom::Playlist::New()); + auto& playlist = playlists.back(); + playlist->name = "default"; + for (const auto& item : value.GetList()) { + auto* dict = item.GetIfDict(); + DCHECK(dict); + DCHECK(dict->contains(playlist::kPlaylistItemIDKey)); + DCHECK(dict->contains(playlist::kPlaylistItemTitleKey)); + DCHECK(dict->contains(playlist::kPlaylistItemMediaFilePathKey)); + DCHECK(dict->contains(playlist::kPlaylistItemThumbnailPathKey)); + + playlist->items.push_back(playlist::mojom::PlaylistItem::New( + *dict->FindString(playlist::kPlaylistItemIDKey) /* = id */, + *dict->FindString(playlist::kPlaylistItemTitleKey) /* = name */, + GURL(*dict->FindString( + playlist::kPlaylistItemMediaFilePathKey)) /* = media_path */, + GURL(*dict->FindString( + playlist:: + kPlaylistItemThumbnailPathKey)) /* = ::GURL& thumbnail_path */, + *dict->FindBool(playlist::kPlaylistItemReadyKey) /* = ready */)); + } + + std::move(callback).Run(std::move(playlists)); +} + +void PlaylistPageHandler::GetPlaylist( + const std::string& id, + PlaylistPageHandler::GetPlaylistCallback callback) { + // TODO(sko) Get items for playlist with |id|. + auto value = GetPlaylistService(profile_)->GetAllPlaylistItems(); + DCHECK(value.is_list()); + playlist::mojom::Playlist playlist; + playlist.name = "default"; + for (const auto& item : value.GetList()) { + auto* dict = item.GetIfDict(); + DCHECK(dict); + DCHECK(dict->contains(playlist::kPlaylistItemIDKey)); + DCHECK(dict->contains(playlist::kPlaylistItemTitleKey)); + DCHECK(dict->contains(playlist::kPlaylistItemMediaFilePathKey)); + DCHECK(dict->contains(playlist::kPlaylistItemThumbnailPathKey)); + + playlist.items.push_back(playlist::mojom::PlaylistItem::New( + *dict->FindString(playlist::kPlaylistItemIDKey) /* = id */, + *dict->FindString(playlist::kPlaylistItemTitleKey) /* = name */, + GURL(*dict->FindString( + playlist::kPlaylistItemMediaFilePathKey)) /* = media_path */, + GURL(*dict->FindString( + playlist:: + kPlaylistItemThumbnailPathKey)) /* = ::GURL& thumbnail_path */, + *dict->FindBool(playlist::kPlaylistItemReadyKey) /* = ready */)); + } + + std::move(callback).Run(playlist.Clone()); +} + +void PlaylistPageHandler::AddMediaFilesFromPageToPlaylist(const std::string& id, + const GURL& url) { + GetPlaylistService(profile_)->RequestDownloadMediaFilesFromPage(id, + url.spec()); +} + +void PlaylistPageHandler::RemoveItemFromPlaylist(const std::string& playlist_id, + const std::string& item_id) { + GetPlaylistService(profile_)->RemoveItemFromPlaylist(playlist_id, item_id); +} + +void PlaylistPageHandler::OnPlaylistItemStatusChanged( + const playlist::PlaylistItemChangeParams& params) { + page_->OnEvent(playlist::mojom::PlaylistEvent::kItemAdded); +} diff --git a/browser/ui/webui/playlist_page_handler.h b/browser/ui/webui/playlist_page_handler.h new file mode 100644 index 00000000000..4d8a5c9948d --- /dev/null +++ b/browser/ui/webui/playlist_page_handler.h @@ -0,0 +1,56 @@ +/* Copyright (c) 2022 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_BROWSER_UI_WEBUI_PLAYLIST_PAGE_HANDLER_H_ +#define BRAVE_BROWSER_UI_WEBUI_PLAYLIST_PAGE_HANDLER_H_ + +#include + +#include "base/scoped_observation.h" +#include "brave/components/playlist/mojom/playlist.mojom.h" +#include "brave/components/playlist/playlist_service.h" +#include "brave/components/playlist/playlist_service_observer.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "mojo/public/cpp/bindings/pending_remote.h" +#include "mojo/public/cpp/bindings/receiver.h" +#include "mojo/public/cpp/bindings/remote.h" + +class Profile; + +class PlaylistPageHandler : public playlist::mojom::PageHandler, + public playlist::PlaylistServiceObserver { + public: + PlaylistPageHandler( + Profile* profile, + mojo::PendingReceiver pending_page_handler, + mojo::PendingRemote pending_page); + ~PlaylistPageHandler() override; + + // playlist::mojom::PageHandler: + void GetAllPlaylists( + PlaylistPageHandler::GetAllPlaylistsCallback callback) override; + void GetPlaylist(const std::string& id, + PlaylistPageHandler::GetPlaylistCallback callback) override; + void AddMediaFilesFromPageToPlaylist(const std::string& playlist_id, + const GURL& url) override; + void RemoveItemFromPlaylist(const std::string& playlist_id, + const std::string& item_id) override; + + // playlist::PlaylistServiceObserver + void OnPlaylistItemStatusChanged( + const playlist::PlaylistItemChangeParams& params) override; + + private: + raw_ptr profile_ = nullptr; + + mojo::Remote page_; + mojo::Receiver handler_; + + base::ScopedObservation + observation_{this}; +}; + +#endif // BRAVE_BROWSER_UI_WEBUI_PLAYLIST_PAGE_HANDLER_H_ diff --git a/browser/ui/webui/playlist_ui.cc b/browser/ui/webui/playlist_ui.cc new file mode 100644 index 00000000000..06f5160a3a8 --- /dev/null +++ b/browser/ui/webui/playlist_ui.cc @@ -0,0 +1,60 @@ +/* Copyright (c) 2021 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, +#include "chrome/browser/profiles/profile.h" + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "brave/browser/ui/webui/playlist_ui.h" + +#include + +#include "brave/browser/playlist/playlist_service_factory.h" +#include "brave/browser/ui/webui/brave_webui_source.h" +#include "brave/browser/ui/webui/playlist_page_handler.h" +#include "brave/components/constants/webui_url_constants.h" +#include "brave/components/playlist/resources/grit/playlist_generated_map.h" +#include "chrome/browser/profiles/profile.h" +#include "components/grit/brave_components_resources.h" +#include "url/gurl.h" + +namespace playlist { + +// static +bool PlaylistUI::ShouldBlockPlaylistWebUI( + content::BrowserContext* browser_context, + const GURL& url) { + if (url.host_piece() != kPlaylistHost) + return false; + + return !PlaylistServiceFactory::IsPlaylistEnabled(browser_context); +} + +PlaylistUI::PlaylistUI(content::WebUI* web_ui, const std::string& name) + : MojoWebUIController(web_ui) { + CreateAndAddWebUIDataSource(web_ui, name, kPlaylistGenerated, + kPlaylistGeneratedSize, IDR_PLAYLIST_HTML); +} + +PlaylistUI::~PlaylistUI() = default; + +void PlaylistUI::BindInterface( + mojo::PendingReceiver + pending_receiver) { + if (page_factory_receiver_.is_bound()) + page_factory_receiver_.reset(); + + page_factory_receiver_.Bind(std::move(pending_receiver)); +} + +void PlaylistUI::CreatePageHandler( + mojo::PendingRemote pending_page, + mojo::PendingReceiver pending_page_handler) { + DCHECK(pending_page.is_valid()); + page_handler_ = std::make_unique( + Profile::FromWebUI(web_ui()), std::move(pending_page_handler), + std::move(pending_page)); +} + +WEB_UI_CONTROLLER_TYPE_IMPL(PlaylistUI) + +} // namespace playlist diff --git a/browser/ui/webui/playlist_ui.h b/browser/ui/webui/playlist_ui.h new file mode 100644 index 00000000000..5123b5ee72f --- /dev/null +++ b/browser/ui/webui/playlist_ui.h @@ -0,0 +1,55 @@ +/* Copyright (c) 2021 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 http://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_BROWSER_UI_WEBUI_PLAYLIST_UI_H_ +#define BRAVE_BROWSER_UI_WEBUI_PLAYLIST_UI_H_ + +#include +#include + +#include "brave/components/playlist/mojom/playlist.mojom.h" +#include "ui/webui/mojo_web_ui_controller.h" + +namespace content { +class BrowserContext; +} // namespace content + +class GURL; +class PlaylistPageHandler; + +namespace playlist { + +class PlaylistUI : public ui::MojoWebUIController, + public playlist::mojom::PageHandlerFactory { + public: + static bool ShouldBlockPlaylistWebUI(content::BrowserContext* browser_context, + const GURL& url); + + PlaylistUI(content::WebUI* web_ui, const std::string& host); + ~PlaylistUI() override; + PlaylistUI(const PlaylistUI&) = delete; + PlaylistUI& operator=(const PlaylistUI&) = delete; + + void BindInterface(mojo::PendingReceiver + pending_receiver); + + // playlist::mojom::PageHandlerFactory: + void CreatePageHandler( + mojo::PendingRemote pending_page, + mojo::PendingReceiver pending_page_handler) + override; + + private: + std::unique_ptr page_handler_; + + mojo::Receiver page_factory_receiver_{ + this}; + + WEB_UI_CONTROLLER_TYPE_DECL(); +}; + +} // namespace playlist + +#endif // BRAVE_BROWSER_UI_WEBUI_PLAYLIST_UI_H_ diff --git a/common/extensions/api/_api_features.json b/common/extensions/api/_api_features.json index b327c0448c4..a7f4dcf31b3 100644 --- a/common/extensions/api/_api_features.json +++ b/common/extensions/api/_api_features.json @@ -162,7 +162,8 @@ "chrome://wallet/*", "chrome://wallet-panel.top-chrome/*", "chrome://brave-shields.top-chrome/*", - "chrome://rewards-panel.top-chrome/*" + "chrome://rewards-panel.top-chrome/*", + "chrome://playlist/*" ] }], "greaselion": [{ diff --git a/components/common/typescript.gni b/components/common/typescript.gni index 04f985bced6..558a8c9fa57 100644 --- a/components/common/typescript.gni +++ b/components/common/typescript.gni @@ -11,6 +11,7 @@ brave_common_web_compile_inputs = [ # see ../definitions/README.md. rebase_path("../definitions/adBlock.d.ts"), rebase_path("../definitions/ipfs.d.ts"), + rebase_path("../definitions/playlist.d.ts"), rebase_path("../definitions/webcompatReporter.d.ts"), rebase_path("../definitions/chromel.d.ts"), rebase_path("../definitions/global.d.ts"), diff --git a/components/constants/webui_url_constants.cc b/components/constants/webui_url_constants.cc index b4e8a877784..808f5a7ad86 100644 --- a/components/constants/webui_url_constants.cc +++ b/components/constants/webui_url_constants.cc @@ -43,3 +43,5 @@ const char kSidebarBookmarksHost[] = "sidebar-bookmarks.top-chrome"; const char kFederatedInternalsURL[] = "brave://federated-internals"; const char kFederatedInternalsHost[] = "federated-internals"; const char kContentFiltersPath[] = "shields/filters"; +const char kPlaylistHost[] = "playlist"; +const char kPlaylistURL[] = "chrome://playlist/"; diff --git a/components/constants/webui_url_constants.h b/components/constants/webui_url_constants.h index 6eeb53428a2..8324d783636 100644 --- a/components/constants/webui_url_constants.h +++ b/components/constants/webui_url_constants.h @@ -45,5 +45,7 @@ extern const char kSidebarBookmarksHost[]; extern const char kFederatedInternalsURL[]; extern const char kFederatedInternalsHost[]; extern const char kContentFiltersPath[]; +extern const char kPlaylistHost[]; +extern const char kPlaylistURL[]; #endif // BRAVE_COMPONENTS_CONSTANTS_WEBUI_URL_CONSTANTS_H_ diff --git a/components/definitions/playlist.d.ts b/components/definitions/playlist.d.ts new file mode 100644 index 00000000000..fae633c5515 --- /dev/null +++ b/components/definitions/playlist.d.ts @@ -0,0 +1,15 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +import * as PlaylistMojo from 'gen/brave/components/playlist/mojom/playlist.mojom.m.js' + +declare namespace Playlist { + export interface ApplicationState { + playlistData: State|undefined + } + + export interface State { + lists : PlaylistMojo.Playlist[] + } +} diff --git a/components/playlist/media_detector_component_manager.cc b/components/playlist/media_detector_component_manager.cc index ce0ccbe45c0..fa9e2a2d4c1 100644 --- a/components/playlist/media_detector_component_manager.cc +++ b/components/playlist/media_detector_component_manager.cc @@ -76,6 +76,59 @@ void MediaDetectorComponentManager::SetUseLocalScriptForTesting() { // https://github.com/brave/brave-ios/blob/development/Client/Frontend/UserContent/UserScripts/Playlist.js static const std::string kScript = R"-( (function() { + function is_nan(value) { + return typeof value === "number" && value !== value; + } + + function is_infinite(value) { + return typeof value === "number" && (value === Infinity || value === -Infinity); + } + + function clamp_duration(value) { + if (is_nan(value)) { + return 0.0; + } + + if (is_infinite(value)) { + return Number.MAX_VALUE; + } + return value; + } + + // Algorithm: + // Generate a random number from 0 to 256 + // Roll-Over clamp to the range [0, 15] + // If the index is 13, set it to 4. + // If the index is 17, clamp it to [0, 3] + // Subtract that number from 15 (XOR) and convert the result to hex. + function uuid_v4() { + // X >> 2 = X / 4 (integer division) + + // AND-ing (15 >> 0) roll-over clamps to 15 + // AND-ing (15 >> 2) roll-over clamps to 3 + // So '8' digit is clamped to 3 (inclusive) and all others clamped to 15 (inclusive). + + // 0 XOR 15 = 15 + // 1 XOR 15 = 14 + // 8 XOR 15 = 7 + // So N XOR 15 = 15 - N + + // UUID string format generated with array appending + // Results in "10000000-1000-4000-8000-100000000000".replace(...) + return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, (X) => { + return (X ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (X >> 2)))).toString(16); + }); + } + + function tagNode(node) { + if (node) { + if (!node.tagUUID) { + node.tagUUID = uuid_v4(); + node.addEventListener('webkitpresentationmodechanged', (e) => e.stopPropagation(), true); + } + } + } + function getNodeSource(node, src, mimeType, thumbnail) { var name = node.title; if (name == null || typeof name == 'undefined' || name == "") { @@ -102,14 +155,14 @@ void MediaDetectorComponentManager::SetUseLocalScriptForTesting() { } if (src && src !== "") { - // tagNode(node); + tagNode(node); return { "name": name, "src": src, "pageSrc": window.location.href, "pageTitle": document.title, "mimeType": mimeType, - //"duration": clamp_duration(node.duration), + "duration": clamp_duration(node.duration), "detected": true, "tagId": node.tagUUID, thumbnail @@ -119,14 +172,14 @@ void MediaDetectorComponentManager::SetUseLocalScriptForTesting() { document.querySelectorAll('source').forEach(function(node) { if (node.src !== "") { if (node.closest('video') === target) { - // tagNode(target); + tagNode(target); return { "name": name, "src": node.src, "pageSrc": window.location.href, "pageTitle": document.title, "mimeType": mimeType, - //"duration": clamp_duration(target.duration), + "duration": clamp_duration(target.duration), "detected": true, "tagId": target.tagUUID, thumbnail @@ -141,7 +194,7 @@ void MediaDetectorComponentManager::SetUseLocalScriptForTesting() { "pageSrc": window.location.href, "pageTitle": document.title, "mimeType": mimeType, - //"duration": clamp_duration(target.duration), + "duration": clamp_duration(target.duration), "detected": true, "tagId": target.tagUUID, thumbnail @@ -186,7 +239,6 @@ void MediaDetectorComponentManager::SetUseLocalScriptForTesting() { return medias; return videoElements; - })(); )-"; diff --git a/components/playlist/mojom/BUILD.gn b/components/playlist/mojom/BUILD.gn new file mode 100644 index 00000000000..c49a7166cc3 --- /dev/null +++ b/components/playlist/mojom/BUILD.gn @@ -0,0 +1,14 @@ +# Copyright (c) 2022 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 http://mozilla.org/MPL/2.0/. + +import("//mojo/public/tools/bindings/mojom.gni") + +mojom("mojom") { + sources = [ "playlist.mojom" ] + public_deps = [ + "//mojo/public/mojom/base", + "//url/mojom:url_mojom_gurl", + ] +} diff --git a/components/playlist/mojom/playlist.mojom b/components/playlist/mojom/playlist.mojom new file mode 100644 index 00000000000..1227996bae6 --- /dev/null +++ b/components/playlist/mojom/playlist.mojom @@ -0,0 +1,61 @@ +// Copyright (c) 2022 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 http://mozilla.org/MPL/2.0/. + +module playlist.mojom; + +import "url/mojom/url.mojom"; + +enum PlaylistEvent { + kItemAdded, + kItemThumbnailReady, + kItemThumbnailFailed, + kItemReadyToPlay, + kItemDeleted, + kAllItemsDeleted, + kItemAborted, +}; + +struct Playlist { + string? id; + string name; + array items; +}; + +struct PlaylistItem { + string id; + string name; + + url.mojom.Url media_path; + url.mojom.Url thumbnail_path; + + // When it's |ready|, |media_path| and |thumbnail_path| will be + // local path. + bool ready; +}; + +// Used by the WebUI page to bootstrap bidirectional communication. +interface PageHandlerFactory { + // The WebUI page's |BrowserProxy| singleton calls this method when the page + // is first initialized. + CreatePageHandler(pending_remote page, + pending_receiver handler); +}; + +// Browser-side handler for requests from WebUI page. +interface PageHandler { + GetAllPlaylists() => (array playlists); + + GetPlaylist(string id) => (Playlist? playlist); + + AddMediaFilesFromPageToPlaylist(string playlist_id, url.mojom.Url url); + + RemoveItemFromPlaylist(string playlist_id, string item_id); +}; + +// WebUI-side handler for requests from the browser. +interface Page { + OnEvent(PlaylistEvent event); +}; + diff --git a/components/playlist/playlist_data_source.cc b/components/playlist/playlist_data_source.cc index 2cc9c0a4385..8743c81cf1d 100644 --- a/components/playlist/playlist_data_source.cc +++ b/components/playlist/playlist_data_source.cc @@ -27,8 +27,10 @@ namespace { scoped_refptr ReadFileToString( const base::FilePath& path) { std::string contents; - if (!base::ReadFileToString(path, &contents)) + if (!base::ReadFileToString(path, &contents)) { + VLOG(2) << __FUNCTION__ << " Failed to read " << path; return nullptr; + } return base::MakeRefCounted( reinterpret_cast(contents.c_str()), @@ -43,7 +45,7 @@ PlaylistDataSource::PlaylistDataSource(PlaylistService* service) PlaylistDataSource::~PlaylistDataSource() {} std::string PlaylistDataSource::GetSource() { - return "playlist-image"; + return "playlist-data"; } void PlaylistDataSource::StartDataRequest( @@ -54,33 +56,78 @@ void PlaylistDataSource::StartDataRequest( std::move(got_data_callback).Run(nullptr); return; } - base::FilePath thumbnail_path; - if (!service_->GetThumbnailPath(URLDataSource::URLToRequestPath(url), - &thumbnail_path)) { + std::string path = URLDataSource::URLToRequestPath(url); + std::string id; + std::string type_string; + if (auto pos = path.find("/"); pos != std::string::npos) { + id = std::string(path.begin(), path.begin() + pos); + type_string = std::string(path.begin() + pos, path.end()); + type_string.erase(std::remove(type_string.begin(), type_string.end(), '/'), + type_string.end()); + } else { + NOTREACHED() << "path is not in expected form: /id/{thumbnail,media}/ vs " + << path; std::move(got_data_callback).Run(nullptr); return; } - GetThumbnailImageFile(thumbnail_path, std::move(got_data_callback)); + + base::FilePath data_path; + if (type_string == "thumbnail") { + if (!service_->GetThumbnailPath(id, &data_path)) { + std::move(got_data_callback).Run(nullptr); + return; + } + } else if (type_string == "media") { + if (!service_->GetMediaPath(id, &data_path)) { + std::move(got_data_callback).Run(nullptr); + return; + } + } else { + NOTREACHED() << "type is neither of {thumbnail,media}/ : " << type_string; + std::move(got_data_callback).Run(nullptr); + return; + } + + GetDataFile(data_path, std::move(got_data_callback)); } -void PlaylistDataSource::GetThumbnailImageFile( - const base::FilePath& image_file_path, - GotDataCallback got_data_callback) { +void PlaylistDataSource::GetDataFile(const base::FilePath& data_path, + GotDataCallback got_data_callback) { base::ThreadPool::PostTaskAndReplyWithResult( - FROM_HERE, base::MayBlock(), - base::BindOnce(&ReadFileToString, image_file_path), - base::BindOnce(&PlaylistDataSource::OnGotThumbnailImageFile, + FROM_HERE, base::MayBlock(), base::BindOnce(&ReadFileToString, data_path), + base::BindOnce(&PlaylistDataSource::OnGotDataFile, weak_factory_.GetWeakPtr(), std::move(got_data_callback))); } -void PlaylistDataSource::OnGotThumbnailImageFile( +void PlaylistDataSource::OnGotDataFile( GotDataCallback got_data_callback, scoped_refptr input) { std::move(got_data_callback).Run(input); } -std::string PlaylistDataSource::GetMimeType(const std::string&) { - return "image/jpg"; +std::string PlaylistDataSource::GetMimeType(const std::string& path) { + std::string id; + std::string type_string; + if (auto pos = path.find("/"); pos != std::string::npos) { + id = std::string(path.begin(), path.begin() + pos); + type_string = std::string(path.begin() + pos, path.end()); + type_string.erase(std::remove(type_string.begin(), type_string.end(), '/'), + type_string.end()); + } else { + NOTREACHED() << "path is not in expected form: /id/{thumbnail,media}/ vs " + << path; + return std::string(); + } + + if (type_string == "thumbnail") + return "image/jpeg"; + + // TODO(sko) Decide mime type based on the file extension. + if (type_string == "media") + return "video/mp4"; + + NOTREACHED() << "type is neither of {thumbnail,media}/ : " << type_string; + return std::string(); } bool PlaylistDataSource::AllowCaching() { diff --git a/components/playlist/playlist_data_source.h b/components/playlist/playlist_data_source.h index 638327fe742..0022af901e3 100644 --- a/components/playlist/playlist_data_source.h +++ b/components/playlist/playlist_data_source.h @@ -22,7 +22,7 @@ namespace playlist { class PlaylistService; -// A URL data source for chrome://playlist-image/ +// A URL data source for chrome://playlist-data//{thumbnail,media}/ // resources, for use in webui pages that want to display downloaded // playlist thumbnail images class PlaylistDataSource : public content::URLDataSource { @@ -37,14 +37,14 @@ class PlaylistDataSource : public content::URLDataSource { void StartDataRequest(const GURL& url, const content::WebContents::Getter& wc_getter, GotDataCallback got_data_callback) override; - std::string GetMimeType(const std::string&) override; + std::string GetMimeType(const std::string& path) override; bool AllowCaching() override; private: - void GetThumbnailImageFile(const base::FilePath& image_file_path, - GotDataCallback got_data_callback); - void OnGotThumbnailImageFile(GotDataCallback got_data_callback, - scoped_refptr input); + void GetDataFile(const base::FilePath& data_path, + GotDataCallback got_data_callback); + void OnGotDataFile(GotDataCallback got_data_callback, + scoped_refptr input); raw_ptr service_; diff --git a/components/playlist/playlist_download_request_manager.cc b/components/playlist/playlist_download_request_manager.cc index 46cd83fcf30..427e093ed67 100644 --- a/components/playlist/playlist_download_request_manager.cc +++ b/components/playlist/playlist_download_request_manager.cc @@ -17,6 +17,7 @@ #include "content/public/common/isolated_world_ids.h" #include "net/base/registry_controlled_domains/registry_controlled_domain.h" #include "third_party/re2/src/re2/re2.h" +#include "ui/base/page_transition_types.h" namespace playlist { @@ -68,6 +69,7 @@ void PlaylistDownloadRequestManager::CreateWebContents() { // |web_contents_| is created on demand. content::WebContents::CreateParams create_params(context_, nullptr); web_contents_ = content::WebContents::Create(create_params); + Observe(web_contents_.get()); } diff --git a/components/playlist/playlist_media_file_download_manager.cc b/components/playlist/playlist_media_file_download_manager.cc index 0c0594e968e..5dbbe8aaea9 100644 --- a/components/playlist/playlist_media_file_download_manager.cc +++ b/components/playlist/playlist_media_file_download_manager.cc @@ -20,10 +20,9 @@ PlaylistMediaFileDownloadManager::PlaylistMediaFileDownloadManager( Delegate* delegate, const base::FilePath& base_dir) : base_dir_(base_dir), delegate_(delegate) { - // TODO(pilgrim) dynamically set file extensions based on format - // (may require changes to youtubedown parser) + // TODO(pilgrim) dynamically set file extensions based on format. media_file_downloader_ = std::make_unique( - this, context, FILE_PATH_LITERAL("media_file.mp4")); + this, context, kMediaFileName); } PlaylistMediaFileDownloadManager::~PlaylistMediaFileDownloadManager() = default; @@ -108,7 +107,7 @@ void PlaylistMediaFileDownloadManager::OnMediaFileReady( const std::string& media_file_path) { VLOG(2) << __func__ << ": " << id << " is ready."; - delegate_->OnMediaFileReady(id, current_item_->media_file_path); + delegate_->OnMediaFileReady(id, media_file_path); current_item_.reset(); diff --git a/components/playlist/playlist_media_file_download_manager.h b/components/playlist/playlist_media_file_download_manager.h index 3f9183b25fa..8fa4e7a1d28 100644 --- a/components/playlist/playlist_media_file_download_manager.h +++ b/components/playlist/playlist_media_file_download_manager.h @@ -40,6 +40,9 @@ class PlaylistMediaFileDownloadManager virtual ~Delegate() {} }; + static constexpr base::FilePath::CharType kMediaFileName[] = + FILE_PATH_LITERAL("media_file.mp4"); + PlaylistMediaFileDownloadManager(content::BrowserContext* context, Delegate* delegate, const base::FilePath& base_dir); diff --git a/components/playlist/playlist_service.cc b/components/playlist/playlist_service.cc index 97335f5fbd2..6c00b0429c9 100644 --- a/components/playlist/playlist_service.cc +++ b/components/playlist/playlist_service.cc @@ -85,6 +85,13 @@ void PlaylistService::RequestDownloadMediaFilesFromPage( download_request_manager_->GetMediaFilesFromPage(url); } +void PlaylistService::RemoveItemFromPlaylist(const std::string& playlist_id, + const std::string& item_id) { + VLOG(2) << __func__ << " " << playlist_id << " " << item_id; + // TODO(sko) consider |playlist_id| + DeletePlaylistItem(item_id); +} + void PlaylistService::OnPlaylistCreationParamsReady( const PlaylistItemInfo& params) { CreatePlaylistItem(params); @@ -293,7 +300,7 @@ void PlaylistService::RemoveObserver(PlaylistServiceObserver* observer) { void PlaylistService::OnMediaFileReady(const std::string& id, const std::string& media_file_path) { - VLOG(2) << __func__ << ": " << id; + VLOG(2) << __func__ << ": " << id << " " << media_file_path; DCHECK(IsValidPlaylistItem(id)); const base::Value* item_value_ptr = @@ -369,6 +376,19 @@ bool PlaylistService::GetThumbnailPath(const std::string& id, return true; } +bool PlaylistService::GetMediaPath(const std::string& id, + base::FilePath* media_path) { + // TODO(sko) The extension of the media file can differ. + // We should iterate files in the directory and find the match. + *media_path = GetPlaylistItemDirPath(id).Append( + PlaylistMediaFileDownloadManager::kMediaFileName); + if (media_path->ReferencesParent()) { + media_path->clear(); + return false; + } + return true; +} + base::SequencedTaskRunner* PlaylistService::task_runner() { if (!task_runner_) { task_runner_ = base::ThreadPool::CreateSequencedTaskRunner( diff --git a/components/playlist/playlist_service.h b/components/playlist/playlist_service.h index 07eecac1ea0..868b5dc5ef8 100644 --- a/components/playlist/playlist_service.h +++ b/components/playlist/playlist_service.h @@ -71,10 +71,14 @@ class PlaylistService : public KeyedService, void RequestDownloadMediaFilesFromPage(const std::string& playlist_id, const std::string& url); + void RemoveItemFromPlaylist(const std::string& playlist_id, + const std::string& item_id); + void AddObserver(PlaylistServiceObserver* observer); void RemoveObserver(PlaylistServiceObserver* observer); bool GetThumbnailPath(const std::string& id, base::FilePath* thumbnail_path); + bool GetMediaPath(const std::string& id, base::FilePath* media_path); base::FilePath GetPlaylistItemDirPath(const std::string& id) const; diff --git a/components/playlist/resources/BUILD.gn b/components/playlist/resources/BUILD.gn new file mode 100644 index 00000000000..e79db8cc49c --- /dev/null +++ b/components/playlist/resources/BUILD.gn @@ -0,0 +1,26 @@ +# Copyright (c) 2021 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 http://mozilla.org/MPL/2.0/. + +import("//brave/components/common/typescript.gni") + +transpile_web_ui("playlist_ui") { + entry_points = [ [ + "playlist", + rebase_path("playlist.tsx"), + ] ] + + deps = [ + "../mojom:mojom_js", + "//mojo/public/mojom/base", + ] + + resource_name = "playlist" +} + +pack_web_resources("generated_resources") { + resource_name = "playlist" + output_dir = "$root_gen_dir/brave/components/playlist/resources" + deps = [ ":playlist_ui" ] +} diff --git a/components/playlist/resources/actions/playlist_action_creators.ts b/components/playlist/resources/actions/playlist_action_creators.ts new file mode 100644 index 00000000000..fca64f2ee9d --- /dev/null +++ b/components/playlist/resources/actions/playlist_action_creators.ts @@ -0,0 +1,13 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +import { action } from 'typesafe-actions' + +import * as PlaylistMojo from 'gen/brave/components/playlist/mojom/playlist.mojom.m.js' + +// Constants +import { types } from '../constants/playlist_types' + +export const playlistLoaded = (playlist: PlaylistMojo.Playlist[]) => + action(types.PLAYLIST_LOADED, playlist) diff --git a/components/playlist/resources/api/api.ts b/components/playlist/resources/api/api.ts new file mode 100644 index 00000000000..0a9c445b1b1 --- /dev/null +++ b/components/playlist/resources/api/api.ts @@ -0,0 +1,66 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +import * as PlaylistMojo from 'gen/brave/components/playlist/mojom/playlist.mojom.m.js' +import { Url } from 'gen/url/mojom/url.mojom.m.js' + +type PlaylistEventListener = (event: PlaylistMojo.PlaylistEvent) => void + +interface API { + pageCallbackRouter: PlaylistMojo.PageCallbackRouter + pageHandler: PlaylistMojo.PageHandlerRemote + + getAllPlaylists: () => Promise<{playlists: PlaylistMojo.Playlist[]}> + getPlaylist: (id: string) => Promise<{playlist?: PlaylistMojo.Playlist}> + addMediaFilesFromPageToPlaylist: (playlistId: string, url: string) => void + removeItemFromPlaylist: (playlistId: string, itemId: string) => void + + addEventListener: (listener: PlaylistEventListener) => void +} + +let playlistAPIInstance: API + +class PlaylistAPIInstance implements API { + pageCallbackRouter = new PlaylistMojo.PageCallbackRouter() + pageHandler = new PlaylistMojo.PageHandlerRemote() + + constructor () { + const factory = PlaylistMojo.PageHandlerFactory.getRemote() + factory.createPageHandler( + this.pageCallbackRouter.$.bindNewPipeAndPassRemote(), + this.pageHandler.$.bindNewPipeAndPassReceiver()) + } + + getAllPlaylists = async function (): + Promise<{playlists: PlaylistMojo.Playlist[]}> { + return this.pageHandler.getAllPlaylists() + } + + getPlaylist = async function (id: string): + Promise<{playlist?: PlaylistMojo.Playlist}> { + return this.pageHandler.getPlaylist(id) + } + + addEventListener (listener: PlaylistEventListener) { + this.pageCallbackRouter.onEvent.addListener(listener) + } + + addMediaFilesFromPageToPlaylist (playlistId: string, url: string) { + let mojoUrl = new Url() + mojoUrl.url = url + this.pageHandler.addMediaFilesFromPageToPlaylist( + playlistId, mojoUrl) + } + + removeItemFromPlaylist (playlistId: string, itemId: string) { + this.pageHandler.removeItemFromPlaylist(playlistId, itemId) + } +} + +export function getPlaylistAPI (): API { + if (!playlistAPIInstance) { + playlistAPIInstance = new PlaylistAPIInstance() + } + return playlistAPIInstance +} diff --git a/components/playlist/resources/api/getAllActions.ts b/components/playlist/resources/api/getAllActions.ts new file mode 100644 index 00000000000..9bdadb3ebe7 --- /dev/null +++ b/components/playlist/resources/api/getAllActions.ts @@ -0,0 +1,22 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +import store from '../store' + +import * as PlaylistActions from '../actions/playlist_action_creators' +import { bindActionCreators, Dispatch } from 'redux' + +type Action = typeof PlaylistActions + +let actions: Action + +// returns actions bound to dispatch. +export function getAllActions () { + actions = actions || getActionsForDispatch(store.dispatch.bind(store)) + return actions +} + +export function getActionsForDispatch (dispatch: Dispatch) { + return bindActionCreators(PlaylistActions, dispatch) +} diff --git a/components/playlist/resources/apiEventsToStore.ts b/components/playlist/resources/apiEventsToStore.ts new file mode 100644 index 00000000000..ae148fc1307 --- /dev/null +++ b/components/playlist/resources/apiEventsToStore.ts @@ -0,0 +1,25 @@ +// Copyright (c) 2022 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 http://mozilla.org/MPL/2.0/. + +import { getAllActions } from './api/getAllActions' +import { getPlaylistAPI } from './api/api' + +async function getInitialData () { + return getPlaylistAPI().getAllPlaylists() +} + +export default function wireApiEventsToStore () { + // Get initial data and dispatch to store + getInitialData() + .then((initialData) => { + getAllActions().playlistLoaded(initialData.playlists) + + // TODO: Add proper event listeners for changes to playlist + getPlaylistAPI().addEventListener((e) => { + getInitialData().then(data => { getAllActions().playlistLoaded(data.playlists) }) + }) + }) + .catch(e => { console.error('New Tab Page fatal error:', e) }) +} diff --git a/components/playlist/resources/components/app.tsx b/components/playlist/resources/components/app.tsx new file mode 100644 index 00000000000..780e5289717 --- /dev/null +++ b/components/playlist/resources/components/app.tsx @@ -0,0 +1,179 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +import * as React from 'react' +import { bindActionCreators, Dispatch } from 'redux' +import { connect } from 'react-redux' + +import { Playlist } from 'components/definitions/playlist' + +// Components for playlist +import { CloseCircleOIcon } from 'brave-ui/components/icons' +import Table, { Cell, Row } from 'brave-ui/components/dataTables/table' +import PlaylistItem from './playlistItem' + +// Utils +import * as playlistActions from '../actions/playlist_action_creators' +import { getPlaylistAPI } from '../api/api' + +import * as PlaylistMojo from 'gen/brave/components/playlist/mojom/playlist.mojom.m.js' + +interface Props { + actions: any + playlistData: Playlist.State +} + +interface State { + playlist?: PlaylistMojo.Playlist + experimentalUrl: string +} + +export class PlaylistPage extends React.Component { + constructor (props: Props) { + super(props) + this.state = { experimentalUrl: '' } + this.onClickDownloadVideo = this.onClickDownloadVideo.bind(this) + } + + get actions () { + return this.props.actions + } + + getCurrentPlaylist = () => { + // TODO(sko): get current playlist index from store + return this.props.playlistData.lists.at(0) + } + + getImgSrc = (itemId: string) => { + return 'chrome://playlist-data/' + itemId + '/thumbnail/' + } + + getMediaSrc = (itemId: string) => { + return 'chrome://playlist-data/' + itemId + '/media' + } + + get lazyButtonStyle () { + const lazyButtonStyle: any = { + alignItems: 'center', + WebkitAppearance: 'none', + width: '50px', + height: '50px', + display: 'flex', + borderRadius: '4px' + } + return lazyButtonStyle + } + + getPlaylistHeader = (): Cell[] => { + return [ + { content: 'INDEX' }, + { content: 'NAME' }, + { content: 'STATUS' }, + { content: 'REMOVE' } + ] + } + + getPlaylistRows = (playlist?: any): Row[] | undefined => { + if (!playlist || !playlist.items) { + return + } + + return playlist.items.map((item: PlaylistMojo.PlaylistItem, index: any): any => { + const cell: Row = { + content: [ + { content: (
{index + 1}
) }, + { + content: ( + + ) + }, + { content: ({item.ready ? 'Ready' : 'Downloading'}) }, + { content: () } + ] + } + return cell + }) + } + + onClickRemoveItemButton = (playlistItemId: string) => { + getPlaylistAPI().removeItemFromPlaylist('', playlistItemId) + } + + get pageHasDownloadableVideo () { + return this.state.experimentalUrl.startsWith('https://www.youtube.com/watch') + } + + onChangeExperimentalUrl = (event: React.ChangeEvent) => { + this.setState({ experimentalUrl: event.target.value }) + } + + onClickDownloadVideo = () => { + getPlaylistAPI().addMediaFilesFromPageToPlaylist('', this.state.experimentalUrl) + } + + onClickItem = (itemId: string) => { + const item = this.getCurrentPlaylist()?.items.find((item: PlaylistMojo.PlaylistItem) => { + return item.id === itemId + }) + + if (!item || !item.ready) { + return + } + + document.getElementById('player')?.setAttribute('src', this.getMediaSrc(itemId)) + } + + render () { + return ( +
+
+ + YOUR PLAYLIST IS EMPTY +
+
+ +