From bce557cc2dc767628bed6aac87301a1be7c5431b Mon Sep 17 00:00:00 2001 From: rxliuli Date: Tue, 4 Nov 2025 05:03:50 +0800 Subject: init commit --- .../tmp/src/common/today/today-card-util.js | 785 +++++++++++++++++++++ 1 file changed, 785 insertions(+) create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/today/today-card-util.js (limited to 'node_modules/@jet-app/app-store/tmp/src/common/today/today-card-util.js') diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/today-card-util.js b/node_modules/@jet-app/app-store/tmp/src/common/today/today-card-util.js new file mode 100644 index 0000000..688f0fb --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/today-card-util.js @@ -0,0 +1,785 @@ +import { isNothing, isSome } from "@jet/environment"; +import * as validation from "@jet/environment/json/validation"; +import { ArtworkContentMode, ExternalUrlAction, FlowAction, TodayCardActionOverlay, TodayCardMediaArtwork, TodayCardMediaHero, TodayCardMediaMultiApp, TodayCardMediaVideo, TodayCardMediaWithArtwork, } from "../../api/models"; +import { asBoolean, asDictionary, asString, isDefinedNonNull, isDefinedNonNullNonEmpty, isNull, isNullOrEmpty, objectPathToString, } from "../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import { relationshipCollection } from "../../foundation/media/relationships"; +import { Parameters } from "../../foundation/network/url-constants"; +import { URL } from "../../foundation/network/urls"; +import * as color from "../../foundation/util/color-util"; +import { artworkFromApiArtwork, editorialNotesFromData, hasMessagesExtensionFromData, isHiddenFromSpringboardFromData, joeColorSetFromData, notesFromData, screenSizeIPhone134, } from "../content/content"; +import { extractEditorialClientParams } from "../editorial-pages/editorial-data-util"; +import * as lockupsEditorialContext from "../lockups/editorial-context"; +import { lockupsFromData } from "../lockups/lockups"; +import * as metricsHelpersLocation from "../metrics/helpers/location"; +import * as metricsHelpersImpressions from "./../metrics/helpers/impressions"; +import { createTodayAppEventCard } from "./cards/today-app-event-card-builder"; +import { createTodayBrandedCard } from "./cards/today-branded-card-builder"; +import { createTodayFullBleedImageCard } from "./cards/today-full-bleed-image-card-builder"; +import { createTodayGridCard } from "./cards/today-grid-card-builder"; +import { createTodayInAppPurchaseCard } from "./cards/today-in-app-purchase-card-builder"; +import { createTodayListCard } from "./cards/today-list-card-builder"; +import { createTodayRiverCard } from "./cards/today-river-card-builder"; +import { createTodayShortImageCard } from "./cards/today-short-image-card-builder"; +import { createTodaySingleAppCard } from "./cards/today-single-app-card-builder"; +import { createTodayVideoCard } from "./cards/today-video-card-builder"; +import { HeroMediaDisplayContext, OfTheDayIntention, TodayCardDisplayStyle, } from "./today-types"; +import { createTodayBaseCard } from "./cards/today-base-card-builder"; +// MARK: - Today Card Creation +/** + * Create a card with some appearance configuration within today page. + * @param {AppStoreObjectGraph} objectGraph The object graph for the app store. + * @param {Data} data to build card of. + * @param {TodayCardConfiguration} cardConfig flags on how to configure a given card. + * @param {TodayParseContext} context to store state updated throughout parsing. + * @param {TodayCardAugmentingData} augmentingData that stores some additional responses that may be used to enhance the contents of `data` + */ +export function todayCardFromData(objectGraph, data, cardConfig, context, augmentingData) { + return validation.catchingContext("todayCardFromData", () => { + const cardDisplayStyle = cardDisplayStyleFromData(data, cardConfig.coercedCollectionTodayCardDisplayStyle); + const clientIdentifier = lockupsEditorialContext.clientIdentifierForEditorialContextInData(objectGraph, data); + // If clientIdentifier is empty, don't override the cardConfig as this may have been set + // at a higher level. + if (isSome(clientIdentifier)) { + if (shouldRespectClientIdentifierOverride(objectGraph, data, clientIdentifier, cardConfig)) { + cardConfig.clientIdentifierOverride = clientIdentifier; + } + else { + cardConfig.clientIdentifierOverride = null; + } + } + // Configure subtitle for cross link. + cardConfig.crossLinkSubtitle = crossLinkSubtitleFromData(objectGraph, data); + let card = null; + switch (cardDisplayStyle) { + case TodayCardDisplayStyle.AppOfTheDay: + case TodayCardDisplayStyle.GameOfTheDay: + card = createTodayBrandedCard(objectGraph, data, cardConfig, context, augmentingData); + break; + case TodayCardDisplayStyle.Video: + card = createTodayVideoCard(objectGraph, data, cardConfig, context, augmentingData); + break; + case TodayCardDisplayStyle.FullBleedImage: + card = createTodayFullBleedImageCard(objectGraph, data, cardConfig, context, augmentingData); + break; + case TodayCardDisplayStyle.InAppPurchase: + card = createTodayInAppPurchaseCard(objectGraph, data, cardConfig, context, augmentingData); + break; + case TodayCardDisplayStyle.AppEventCard: + card = createTodayAppEventCard(objectGraph, data, cardConfig, context, augmentingData); + break; + case TodayCardDisplayStyle.List: + case TodayCardDisplayStyle.NumberedList: + card = createTodayListCard(objectGraph, data, cardConfig, context, augmentingData); + break; + case TodayCardDisplayStyle.River: + case TodayCardDisplayStyle.Grid: + if (objectGraph.client.isMac) { + card = createTodayGridCard(objectGraph, data, cardConfig, context, augmentingData); + } + else { + card = createTodayRiverCard(objectGraph, data, cardConfig, context, augmentingData); + } + break; + case TodayCardDisplayStyle.SingleApp: + card = createTodaySingleAppCard(objectGraph, data, cardConfig, context, augmentingData); + break; + case TodayCardDisplayStyle.ShortImage: + card = createTodayShortImageCard(objectGraph, data, cardConfig, context, augmentingData); + break; + default: + card = null; + break; + } + if (isNull(card)) { + objectGraph.console.log(`Unknown style: ${cardDisplayStyle}`); + return card; + } + // For certain platforms we add hero media to the card. + addHeroMediaToTodayCardIfNecessary(objectGraph, card, cardConfig, data); + addExternalLinkOverlayToTodayCardIfNecessary(card); + addOTDStyleToCardIfNecessary(card, cardConfig); + enableFlipAndBlurIfNecessary(objectGraph, card); + if (isNothing(card.media)) { + objectGraph.console.log(`Missing required media: ${cardDisplayStyle}`); + card = null; + } + return card; + }, (error) => { + objectGraph.console.log(error); + return null; + }); +} +/** + * On the watch we need the todayCard on the article to get the title and subtitle, + * so even if we dont successfully make a card for the article we create a basic one with an empty media so the title / subtitle can be used + * @param {AppStoreObjectGraph} objectGraph The object graph for the app store. + * @param {Data} data to build card of. + * @param {TodayCardConfiguration} cardConfig flags on how to configure a given card. + * @param {TodayParseContext} context to store state updated throughout parsing. + */ +export function fallbackWatchTodayCardFromData(objectGraph, data, cardConfig, context) { + if (!objectGraph.client.isWatch) { + return null; + } + const fallbackCard = createTodayBaseCard(objectGraph, data, cardConfig, context); + fallbackCard.media = new TodayCardMediaArtwork([], [], []); + return fallbackCard; +} +/** + * Create the {EditorialDisplayOptions for a card given the MAPI data + * @param objectGraph The dependency graph for the App Store + * @param data The MAPI data for this today card + * @param cardConfig The configuration for the card + * @returns The editorialDisplayOptions for this card + */ +export function todayCardEditorialDisplayOptionsFromData(objectGraph, data, cardConfig) { + var _a; + // here is where we make the display options + const editorialClientParams = extractEditorialClientParams(objectGraph, data); + const isExtraWideCard = objectGraph.client.isPad && (cardConfig === null || cardConfig === void 0 ? void 0 : cardConfig.isHeroCard); + const editorialDisplayOptions = { + suppressTagline: mediaAttributes.attributeAsBoolean(data, "ignoreITunesShortNotes"), + suppressLockup: asBoolean(editorialClientParams["suppressLockup"]), + showBadgeInSmallCards: (_a = cardConfig.alwaysShowBadgeInSmallCards) !== null && _a !== void 0 ? _a : asBoolean(editorialClientParams["showBadgeInSmallCards"]), + useMaterialBlur: cardConfig.alwaysUseMaterialBlur || isExtraWideCard || asBoolean(editorialClientParams["useMaterialBlur"]), + }; + return editorialDisplayOptions; +} +// MARK: - Today Configuration +/** + * Create the "default" card configuration that callers can generally base their configuration from. + */ +export function defaultTodayCardConfiguration(objectGraph) { + return { + useOTDTextStyle: false, + enableFullScreenVideo: true, + enableListCardToMultiAppFallback: true, + canDisplayArcadeOfferButton: true, + isHeroCard: false, + }; +} +// MARK: - Metrics / Location Tracking +/** + * Allows for specialized card builders to share the same location tracking push / pop code. + * + * @param objectGraph The dependency graph for the App Store + * @param data The media api data to get location information from + * @param cardConfig The configuration for the card + * @param context The parse context for the over all today page + */ +export function pushTodayCardLocation(objectGraph, data, cardConfig, context, titleOverride) { + const title = isDefinedNonNullNonEmpty(titleOverride) + ? titleOverride + : todayCardTitleFromData(objectGraph, data, cardDisplayStyleFromData(data, cardConfig.coercedCollectionTodayCardDisplayStyle)); + metricsHelpersLocation.pushContentLocation(objectGraph, todayCardMetricsOptions(objectGraph, data, cardConfig, context, title), title !== null && title !== void 0 ? title : ""); +} +export function todayCardMetricsOptions(objectGraph, data, cardConfig, context, titleOverride) { + var _a; + return metricsHelpersImpressions.impressionOptions(objectGraph, data, titleOverride !== null && titleOverride !== void 0 ? titleOverride : "", { + ...cardConfig === null || cardConfig === void 0 ? void 0 : cardConfig.baseMetricsOptions, + targetType: "todayCard", + pageInformation: context.pageInformation, + locationTracker: context.locationTracker, + isAdEligible: (_a = cardConfig === null || cardConfig === void 0 ? void 0 : cardConfig.isAdEligible) !== null && _a !== void 0 ? _a : false, + optimizationId: asString(data, "meta.personalizationData.optimizationId"), + optimizationEntityId: asString(data, "meta.personalizationData.optimizationEntityId"), + rowIndex: cardConfig === null || cardConfig === void 0 ? void 0 : cardConfig.currentRowIndex, + displayStyle: cardConfig === null || cardConfig === void 0 ? void 0 : cardConfig.metricsDisplayStyle, + }); +} +/** + * Pop the today card location that was pushed by the pushTodayCardLocation function. + * + * @param context The parse context for the over all today page + */ +export function popTodayCardLocation(context) { + metricsHelpersLocation.popLocation(context.locationTracker); +} +// MARK: - Card Title +/** + * @param objectGraph The dependency graph for the App Store + * @param data The media api data used to determine the title of the card + * @param cardDisplayStyle The display style of the card + * @returns The title of the card + */ +export function todayCardTitleFromData(objectGraph, data, cardDisplayStyle) { + let title = notesFromData(objectGraph, data, "name"); + if (isNullOrEmpty(title)) { + switch (cardDisplayStyle) { + case TodayCardDisplayStyle.AppOfTheDay: + case TodayCardDisplayStyle.GameOfTheDay: + title = mediaAttributes.attributeAsString(data, "label"); + break; + default: + break; + } + } + return title; +} +// MARK: - Card Display Styles +/** + * These are today card types that can be coerced into an override type, there are times where we want to + * have all List cards for instance, render as grids, this set denotes the types that can be coerced. + */ +const coercibleTodayCardStyles = new Set([ + TodayCardDisplayStyle.Grid, + TodayCardDisplayStyle.List, + TodayCardDisplayStyle.NumberedList, + TodayCardDisplayStyle.River, +]); +/** + * Find the today card style for the given data, allowing coercion if necessary. + * @param data The media api data for an editorial item + * @param styleOverride The override style to use if the data has a style that can be coerced + * @returns The resolved todayCardDisplayStyle + */ +export function cardDisplayStyleFromData(data, styleOverride) { + const cardDisplayStyle = mediaAttributes.attributeAsString(data, "cardDisplayStyle"); + if (coercibleTodayCardStyles.has(cardDisplayStyle) && isSome(styleOverride)) { + return styleOverride; + } + return cardDisplayStyle; +} +// MARK: - Collection Cards +/** + * @param objectGraph Dependency graph for the App Store + * @param data MAPI data for the card these lockups are included in + * @param cardConfig The configuration for the card these lockups are included in + * @param context The parse context for the over all today page + * @param includeLockupClickActions Whether we should include the click actions for the lockups, this is only needed + * when a card allows clicks on a lockup + * @returns The list of lockups for the collection displayed on a TodayCard + */ +export function lockupsForCollectionCardFromData(objectGraph, data, cardConfig, context, includeLockupClickActions) { + const relatedContent = relatedCardContentsContentsFromData(objectGraph, data); + const filteredRelatedContent = relatedContent.filter((cardData) => { + const isHiddenFromSpringboard = isHiddenFromSpringboardFromData(objectGraph, cardData); + const hasMessagesExtension = hasMessagesExtensionFromData(objectGraph, cardData); + return !hasMessagesExtension || !isHiddenFromSpringboard; + }); + return lockupsForRelatedContent(objectGraph, filteredRelatedContent, cardConfig, context.pageInformation, context.locationTracker, undefined, undefined, undefined, includeLockupClickActions); +} +/** + * @param objectGraph Dependency graph for the App Store + * @param relatedContent The list of MAPI data objects to generate lockups for + * @param cardConfig The configuration for the card these lockups are included in + * @param pageInformation The pageInformation for the page these lockups are included in + * @param locationTracker The locationTracker used for impressions on these lockups + * @param offerEnvironment The offer environment to use for the lockups + * @param offerStyle The offerStyle to use for the lockups + * @param externalDeepLinkUrl The promotional deep link url to use on the lockup's offer. + * @param includeLockupClickActions Whether we should include the click actions for the lockups, this is only needed + * when a card allows clicks on a lockup + * @returns The list of lockups for the collection displayed on a TodayCard + */ +export function lockupsForRelatedContent(objectGraph, relatedContent, cardConfig, pageInformation, locationTracker, offerEnvironment, offerStyle, externalDeepLinkUrl, includeLockupClickActions = true) { + if (isNullOrEmpty(relatedContent)) { + return []; + } + // IAPs are only suitable for the TodayCardInAppPurchase card. That card follows a separate path, so we should + // filter them out here. + const filteredRelatedContent = relatedContent.filter((cardData) => { + if (isDefinedNonNullNonEmpty(cardData.attributes)) { + return cardData.type !== "in-apps"; + } + return true; + }); + const options = { + lockupOptions: { + metricsOptions: { + pageInformation: pageInformation, + locationTracker: locationTracker, + }, + offerEnvironment: offerEnvironment, + offerStyle: offerStyle, + clientIdentifierOverride: cardConfig.clientIdentifierOverride, + externalDeepLinkUrl: externalDeepLinkUrl, + crossLinkSubtitle: cardConfig.crossLinkSubtitle, + artworkUseCase: 1 /* ArtworkUseCase.LockupIconSmall */, + canDisplayArcadeOfferButton: cardConfig.canDisplayArcadeOfferButton, + useJoeColorIconPlaceholder: cardConfig.useJoeColorIconPlaceholder, + skipDefaultClickAction: !includeLockupClickActions, + }, + filter: 76670 /* Filter.TodayCard */, + }; + return lockupsFromData(objectGraph, filteredRelatedContent, options); +} +// MARK: - Related Content +/** + * @param objectGraph Dependency graph for the App Store + * @param data MAPI data to get the relationship from + * @returns The card-contents relationship from the MAPI data + */ +export function relatedCardContentsContentsFromData(objectGraph, data) { + return relationshipCollection(data, "card-contents"); +} +// MARK: - Grid / List Collection Card Fallback +/** + * @param objectGraph The object graph for the app store + * @returns The min icon count that a Grid card should use before falling back to fallback media + */ +export function gridFallbackLimit(objectGraph) { + switch (objectGraph.client.deviceType) { + case "tv": + return 2; + default: + return 4; + } +} +/** + * @param objectGraph The object graph for the app store + * @returns The min icon count that a List card should use before falling back to fallback media + */ +export function listFallbackLimit(objectGraph) { + // Limits before using fallback media card for list-types and grid-types (including river). + switch (objectGraph.client.deviceType) { + case "tv": + return 3; + default: + return 4; + } +} +/** + * When we do not have enough content to render a list or grid card, we will use a multi + * app fallback media in its place. This also updates the click action to include a param + * indicating we're using fallback media. + * + * @param objectGraph The object graph for the app store + * @param data The MAPI data for the card + * @param fallbackItems The list of lockups to use as fallback media + * @param card The base card to apply the fallback media to + */ +export function applyMultiAppFallbackToCollectionCard(objectGraph, data, fallbackItems, card) { + const extraText = notesFromData(objectGraph, data, "short"); + card.media = new TodayCardMediaMultiApp(fallbackItems, extraText); + card.style = "dark"; + if (card.clickAction instanceof FlowAction) { + const updatedPageUrl = URL.from(card.clickAction.pageUrl); + updatedPageUrl.param(Parameters.showingFallbackMedia, "true"); + card.clickAction.pageUrl = updatedPageUrl.build(); + } +} +// MARK: - Editorial Artwork +/** + * @param objectGraph The object graph for the app store + * @param cardDisplayStyle The display style for the card we're getting the key path for + * @returns The keypath used to find the editorial artwork for the card + */ +export function editorialArtKeyPathForCardDisplayStyle(objectGraph, cardDisplayStyle) { + if (objectGraph.client.isWatch) { + return "editorialArtwork.subscriptionHero"; + } + else if (objectGraph.client.isVision) { + return "editorialArtwork.storyCenteredStatic16x9"; + } + else { + switch (cardDisplayStyle) { + case TodayCardDisplayStyle.AppOfTheDay: + case TodayCardDisplayStyle.GameOfTheDay: + return "editorialArtwork.dayCard"; + case TodayCardDisplayStyle.AppEventCard: + return "editorialArtwork.eventCard"; + case TodayCardDisplayStyle.Video: + case TodayCardDisplayStyle.FullBleedImage: + return "editorialArtwork.mediaCard"; + default: + return "editorialArtwork.generalCard"; + } + } +} +/** + * @param objectGraph The object graph for the app store + * @param artworkData The artwork data to create the artwork from, from the cards media api data + * @param cropCode The crop code to use for the artwork, otherwise we use the correct crop for the platform + * @returns The artwork model for the card + */ +export function todayCardArtworkFromArtworkData(objectGraph, artworkData, cropCode) { + if (isNullOrEmpty(artworkData)) { + return null; + } + const artwork = artworkFromApiArtwork(objectGraph, artworkData, { + withJoeColorPlaceholder: true, + useCase: 15 /* ArtworkUseCase.TodayCardMedia */, + }); + if (cropCode) { + artwork.crop = cropCode; + } + else if (objectGraph.client.isMac || objectGraph.client.isTV) { + // Articles: Use new crossover crop code for macOS story card art + artwork.crop = "fn"; + } + else { + artwork.crop = "sr"; + } + return artwork; +} +/** + * Returns a branded title `Artwork` model object from the API `editorialArtwork.contentLogoTrimmed` response. + * This artwork can be used in place of a text title for a Today Card. + * @param objectGraph The dependency graph for the App Store + * @param data The media API data to fetch the Artwork from + * @returns The branded title `Artwork` object, or `undefined` if a branded title could not be found + */ +export function brandedTitleArtworkForCard(objectGraph, data) { + const brandedTitleData = mediaAttributes.attributeAsDictionary(data, "editorialArtwork.contentLogoTrimmed"); + return artworkFromApiArtwork(objectGraph, brandedTitleData, { + contentMode: ArtworkContentMode.scaleAspectFit, + allowingTransparency: true, + useCase: 17 /* ArtworkUseCase.TodayCardBrandedTitle */, + }); +} +/** + * @param objectGraph The object graph for the app store + * @param data The media api data for the card + * @param cardConfig The configuration for the card being built + * @returns The `bgGradientKind` field from the correct artwork dictionary + */ +export function todayCardArtworkTitleBackingGradientForKey(objectGraph, data, cardConfig) { + const cardDisplayStyle = cardDisplayStyleFromData(data, cardConfig.coercedCollectionTodayCardDisplayStyle); + const artworkKey = editorialArtKeyPathForCardDisplayStyle(objectGraph, cardDisplayStyle); + const artworkData = mediaAttributes.attributeAsDictionary(data, artworkKey); + if (!isDefinedNonNull(artworkData)) { + return null; + } + return asString(artworkData, "bgGradientKind"); +} +/** + * @param objectGraph The object graph for the app store + * @param artworkData The artwork data from the media api data for a card, that allows us to determine the style + * @returns The light or dark style for this card, if we can determine it. + */ +export function todayCardStyleFromArtwork(objectGraph, artworkData) { + var _a; + if (isNullOrEmpty(artworkData)) { + return undefined; + } + const joeColors = joeColorSetFromData(artworkData); + const backgroundColor = joeColors.bgColor; + const hasGradient = ((_a = joeColors.textGradient) === null || _a === void 0 ? void 0 : _a.length) === 2; + if (!backgroundColor && !hasGradient) { + return undefined; + } + if (objectGraph.client.isiOS || objectGraph.client.isWeb) { + return cardStyleFromJoeColors(joeColors, "bgColor"); + } + else if (hasGradient) { + // Gradient colors are the text color, so if the gradient is a dark color, then + // the environment is a light environment + return color.isDarkColor(joeColors.textGradient[0]) ? "light" : "dark"; + } + else { + return color.isDarkColor(backgroundColor) ? "dark" : "light"; + } +} +export function cardStyleFromJoeColorsWithoutFallback(joeColors, preferredColorKeypath = "bgColor") { + if (isNothing(joeColors)) { + return undefined; + } + if (isSome(joeColors === null || joeColors === void 0 ? void 0 : joeColors.textGradient) && joeColors.textGradient.length === 2) { + // Rare, but if gradient colors are present, defer to them + return color.isDarkColor(joeColors.textGradient[0]) ? "white" : "dark"; + } + const preferredColor = joeColors[preferredColorKeypath]; + if (isNothing(preferredColor)) { + return undefined; + } + const luminance = color.luminanceFrom(preferredColor); + if (luminance <= 0.1) { + return "dark"; + } + else if (luminance >= 0.84) { + return "white"; + } + else { + return "light"; + } +} +export function cardStyleFromJoeColors(joeColors, preferredColorKeypath = "bgColor") { + var _a; + return (_a = cardStyleFromJoeColorsWithoutFallback(joeColors, preferredColorKeypath)) !== null && _a !== void 0 ? _a : "light"; +} +// MARK: - Editorial Text +/** + * Determines the heading for the card, were it to appear inline. + * If the source card heading is null, we default to the existing + * inline heading, if any. + * @param objectGraph The object graph for the app store + * @param card The today card we're collapsing the heading for + * @returns string The title to use for the card. + */ +export function collapsedHeadingForTodayCard(objectGraph, card) { + if (isDefinedNonNull(card.heading)) { + return card.heading.replace(/\n/g, " "); + } + return card.collapsedHeading; +} +// MARK: - Offers +/** + * Determines an appropriate OfferStyle for a TodayCard to ensure correct + * contrast with the card's contents. + * + * @param cardStyle The today style for the card + * @returns an appropriate offer style to use for the button + */ +export function offerStyleForTodayCard(objectGraph, cardStyle) { + return "transparent"; +} +/** + * Determines an appropriate OfferEnvironment for a TodayCard to ensure correct + * contrast with the card's contents. + * + * @param cardStyle The today style for the card + * @returns an appropriate offer environment to use for the button + */ +export function offerEnvironmentForTodayCard(cardStyle) { + if (cardStyle === "white") { + return "light"; + } + else { + return "todayCard"; + } +} +// MARK: - Hero Media +/** + * For mac and tv platforms, we add hero media to the today cards. + * + * @param objectGraph The dependency graph for the app store + * @param card The today card we're adding hero media to + * @param cardConfig The config used to create this card + * @param data The media api data used to create this card + */ +export function addHeroMediaToTodayCardIfNecessary(objectGraph, card, cardConfig, data) { + const cardDisplayStyle = cardDisplayStyleFromData(data, cardConfig.coercedCollectionTodayCardDisplayStyle); + // MacOS and tvOS: Hero + const heroArt = todayCardHeroArtForData(objectGraph, data, cardConfig.heroDisplayContext, cardDisplayStyle, cardConfig.prevailingCropCodes); + let artworks = []; + let videos = []; + if (isDefinedNonNull(heroArt)) { + artworks = [heroArt]; + } + if (isDefinedNonNull(card.media) && + (card.media instanceof TodayCardMediaVideo || card.media instanceof TodayCardMediaArtwork)) { + videos = card.media.videos; + } + const hero = new TodayCardMediaHero(artworks, videos); + const supportsHeroMedia = objectGraph.client.isMac || + objectGraph.client.isTV || + objectGraph.client.isWeb || + (objectGraph.client.isVision && !cardConfig.isSearchContext) || + preprocessor.GAMES_TARGET; + if (supportsHeroMedia && hero.isValid()) { + card.heroMedia = hero; + // Ensure we update the card styling. We prefer hero art, and fallback to hero video. + const heroArtworkData = mediaAttributes.attributeAsDictionary(data, heroMediaArtworkKeyForContext(objectGraph, cardConfig.heroDisplayContext, cardDisplayStyle, Object.keys(mediaAttributes.attributeAsDictionary(data, "editorialArtwork")))); + let style = todayCardStyleFromArtwork(objectGraph, heroArtworkData); + if (!isDefinedNonNull(style)) { + const heroVideoPreviewData = asDictionary(todayCardHeroVideoFromData(objectGraph, data), "previewFrame"); + style = todayCardStyleFromArtwork(objectGraph, heroVideoPreviewData); + } + card.style = style; + } +} +/** + * For external links we will display the standard link overlay in place of whatever overlay we had previously + * + * @param card The today card we're adding hero media to + */ +export function addExternalLinkOverlayToTodayCardIfNecessary(card) { + if (card.clickAction instanceof ExternalUrlAction) { + card.overlay = new TodayCardActionOverlay(card.clickAction); + card.style = "white"; + } +} +/** + * @param objectGraph The dependency graph for the app store + * @param data The media api data to search for hero art in + * @param context The context in which this hero media will be displayed + * @param displayStyle The display style of the card + * @param prevailingCropCodes The prevailing crop code to use for inline hero media + * @returns The artwork to be used in the hero position for a today card + */ +export function todayCardHeroArtForData(objectGraph, data, context, displayStyle, prevailingCropCodes) { + const heroArtworkKey = heroMediaArtworkKeyForContext(objectGraph, context, displayStyle, Object.keys(mediaAttributes.attributeAsDictionary(data, "editorialArtwork"))); + const heroArtworkData = mediaAttributes.attributeAsDictionary(data, heroArtworkKey); + return todayCardArtworkFromArtworkData(objectGraph, heroArtworkData, heroMediaArtworkCropForContext(objectGraph, context, objectPathToString(heroArtworkKey), prevailingCropCodes)); +} +/** + * @param objectGraph The dependency graph for the app store + * @param context The context in which this hero media will be displayed + * @param displayStyle The display style of the card + * @returns The key to use to find the hero media artwork + */ +function heroMediaArtworkKeyForContext(objectGraph, context, displayStyle, availableArtworkKeys) { + if (context === "article" && (objectGraph.client.isMac || objectGraph.client.isiOS)) { + // Use "iOS" art + return editorialArtKeyPathForCardDisplayStyle(objectGraph, displayStyle); + } + else if (objectGraph.client.isVision) { + return "editorialArtwork.heroStatic16x9"; + } + else { + if (objectGraph.client.isTV && availableArtworkKeys.includes("categoryDetailStatic16x9")) { + return "editorialArtwork.categoryDetailStatic16x9"; + } + return "editorialArtwork.crossoverCard"; + } +} +/** + * @param objectGraph The dependency graph for the app store + * @param context The context in which this hero media will be displayed + * @param prevailingCropCodes The crop code to use if we're not overriding it + * @returns The crop code to use for hero artwork in a given context + */ +function heroMediaArtworkCropForContext(objectGraph, context, heroArtworkKey, prevailingCropCodes) { + if (context === HeroMediaDisplayContext.Article && objectGraph.client.isMac) { + // Use "iOS" art crop code + return "fn"; + } + else if (context === HeroMediaDisplayContext.Article && + objectGraph.client.isTV && + heroArtworkKey === "editorialArtwork.categoryDetailStatic16x9") { + return "sr"; + } + else { + return prevailingCropCodes === null || prevailingCropCodes === void 0 ? void 0 : prevailingCropCodes.defaultCrop; + } +} +/** + * @param objectGraph The dependency graph for the app store + * @param data The media api data to search for hero video in + * @returns The video to be used in the hero position for a today card + */ +function todayCardHeroVideoFromData(objectGraph, data) { + let videoData; + const videoData4x3 = mediaAttributes.attributeAsDictionary(data, "editorialVideo.storeFrontVideo4x3"); + const videoData16x9 = mediaAttributes.attributeAsDictionary(data, "editorialVideo.storeFrontVideo"); + // Today: Video Card: Incorrect key for 4x3 videos + // On pad prefer 4x3 fallback to 16x9 and on phone vice versa + if (objectGraph.client.isPad || objectGraph.client.screenSize.isEqualTo(screenSizeIPhone134)) { + videoData = videoData4x3 || videoData16x9; + } + else { + videoData = videoData16x9 || videoData4x3; + } + return videoData; +} +// MARK: - Crosslinks +/** + * Determines the cross link subtitle from card data. + */ +export function crossLinkSubtitleFromData(objectGraph, data) { + // Start with short notes + let subtitle = editorialNotesFromData(objectGraph, data, "short"); + // Fallback to name + if (!isDefinedNonNullNonEmpty(subtitle)) { + subtitle = notesFromData(objectGraph, data, "name"); + } + // Fallback to label + if (!isDefinedNonNullNonEmpty(subtitle)) { + const cardDisplayStyle = mediaAttributes.attributeAsString(data, "displayStyle"); + if (cardDisplayStyle === TodayCardDisplayStyle.AppOfTheDay || + cardDisplayStyle === TodayCardDisplayStyle.GameOfTheDay) { + subtitle = mediaAttributes.attributeAsString(data, "label"); + } + } + return subtitle; +} +// MARK: - Client Identifier Overrids +function shouldRespectClientIdentifierOverride(objectGraph, data, clientIdentifier, cardConfig) { + var _a; + if (clientIdentifier === "com.apple.visionproapp" /* ClientIdentifier.VisionCompanion */ || clientIdentifier === "VisionAppStore" /* ClientIdentifier.VisionAppStore */) { + // If using the companion app, it's expected that we always respect the given identifier. + return true; + } + const relatedContent = relatedCardContentsContentsFromData(objectGraph, data); + const cardDisplayStyle = cardDisplayStyleFromData(data, cardConfig.coercedCollectionTodayCardDisplayStyle); + // Do not apply the override if this is not a Content type card. + const cardTypesRequiringClientIdentifierOverride = new Set([ + TodayCardDisplayStyle.AppEventCard, + TodayCardDisplayStyle.Grid, + TodayCardDisplayStyle.InAppPurchase, + TodayCardDisplayStyle.List, + TodayCardDisplayStyle.NumberedList, + TodayCardDisplayStyle.River, + TodayCardDisplayStyle.ShortImage, + TodayCardDisplayStyle.SingleApp, + ]); + if (!cardTypesRequiringClientIdentifierOverride.has(cardDisplayStyle)) { + return false; + } + // Grids with >= 6 items are unsupported + if (relatedContent.length >= 6 && cardDisplayStyle === TodayCardDisplayStyle.Grid) { + return false; + } + // Content cards with Artwork are unsupported + const artworkKey = editorialArtKeyPathForCardDisplayStyle(objectGraph, cardDisplayStyle); + if (!mediaAttributes.attributeAsBooleanOrFalse(data, "ignoreEditorialArt") && + todayCardArtworkFromArtworkData(objectGraph, mediaAttributes.attributeAsDictionary(data, artworkKey), (_a = cardConfig.prevailingCropCodes) === null || _a === void 0 ? void 0 : _a.defaultCrop)) { + return false; + } + // Content cards with iAP are unsupported + if (inAppPurchaseDataFromRelatedContent(objectGraph, relatedContent)) { + return false; + } + return true; +} +function inAppPurchaseDataFromRelatedContent(objectGraph, relatedContent) { + if (relatedContent.length === 1) { + const contentData = relatedContent[0]; + if (contentData.type === "in-apps") { + return contentData; + } + } + return null; +} +/** + * If a card should be using the OTD style we need to make sure we set that on the card + * + * @param card The today card we're adding the style to + * @param cardConfig The config used to create this card + */ +function addOTDStyleToCardIfNecessary(card, cardConfig) { + if (isNothing(card.media)) { + return; + } + card.media.otdTextStyle = cardConfig.useOTDTextStyle; +} +/** + * Check if this card should have the flip and blur enabled, this is the case when a card has videos. + * @param objectGraph The dependency graph for the app store + * @param card The today card we're modifying + */ +function enableFlipAndBlurIfNecessary(objectGraph, card) { + const cardMedia = card.media; + const cardMediaHasArtwork = cardMedia instanceof TodayCardMediaWithArtwork; + if (!cardMediaHasArtwork) { + return; + } + const cardMdiaWithArtwork = cardMedia; + const hasVideos = isDefinedNonNullNonEmpty(cardMdiaWithArtwork.videos); + card.supportsMediaMirroring = hasVideos; +} +/** + * @param data The media API data used to determine the intention of the card + * @param cardConfig The configuration for the card + * @returns Whether this app is an App of the Day or Game of the Day + */ +export function isCardOTDIntention(data, cardConfig) { + let otdIntention = mediaAttributes.attributeAsString(data, "ofTheDayIntent"); + if (isNothing(otdIntention)) { + const cardDisplayStyle = cardDisplayStyleFromData(data, cardConfig === null || cardConfig === void 0 ? void 0 : cardConfig.coercedCollectionTodayCardDisplayStyle); + switch (cardDisplayStyle) { + case TodayCardDisplayStyle.AppOfTheDay: + otdIntention = OfTheDayIntention.AppOfTheDay; + break; + case TodayCardDisplayStyle.GameOfTheDay: + otdIntention = OfTheDayIntention.GameOfTheDay; + break; + default: + break; + } + } + return otdIntention === OfTheDayIntention.AppOfTheDay || otdIntention === OfTheDayIntention.GameOfTheDay; +} +//# sourceMappingURL=today-card-util.js.map \ No newline at end of file -- cgit v1.2.3