├── types └── messages │ └── index.ts ├── @spotify-internal ├── uri │ ├── src │ │ ├── uri_typedefs.js │ │ ├── enums │ │ │ ├── uri_format.ts │ │ │ ├── uri_particle.js │ │ │ ├── uri_particle.ts │ │ │ ├── uri_format.js │ │ │ ├── prefix.js │ │ │ └── prefix.ts │ │ └── _internal │ │ │ ├── helpers.js │ │ │ └── helpers.ts │ ├── index.ts │ └── index.js ├── encore-web │ ├── types │ │ └── src │ │ │ └── core │ │ │ └── components │ │ │ └── Icon │ │ │ ├── Svg.js │ │ │ └── Svg.ts │ └── es │ │ ├── components │ │ ├── FormToggle │ │ │ ├── Checkbox.js │ │ │ ├── Indicator.js │ │ │ └── Label.js │ │ ├── Icon │ │ │ ├── deprecated-encore-web │ │ │ │ ├── IconMicrophone │ │ │ │ │ └── index.js │ │ │ │ ├── IconUserAltCircle │ │ │ │ │ └── index.js │ │ │ │ ├── IconHeartAltActive │ │ │ │ │ └── index.js │ │ │ │ ├── IconCheckAltActive │ │ │ │ │ └── index.js │ │ │ │ ├── IconDownloadAltActive │ │ │ │ │ └── index.js │ │ │ │ └── IconExclamationAlt │ │ │ │ │ └── index.js │ │ │ └── Svg.js │ │ ├── LogoSpotify │ │ │ └── Svg.js │ │ ├── ButtonPrimary │ │ │ ├── ButtonFocus.js │ │ │ └── ButtonChildren.js │ │ ├── GlobalStyles │ │ │ └── index.js │ │ └── ButtonSecondary │ │ │ └── ButtonChildren.js │ │ ├── styles │ │ ├── mixins │ │ │ ├── baseline.js │ │ │ ├── visuallyHidden.js │ │ │ └── localization.js │ │ ├── index.js │ │ ├── variables.js │ │ └── global-styles.js │ │ └── contexts │ │ ├── KeyboardDetectionContext.js │ │ ├── BrowserDefaultFocusStyleContext.js │ │ └── EncoreContext.js ├── ubi-logger-js │ └── src │ │ ├── providers │ │ ├── PlaybackIdProvider.js │ │ ├── PlayContextUriProvider.js │ │ ├── PlaybackIdProvider.ts │ │ ├── PlayContextUriProvider.ts │ │ ├── PageInstanceIdProvider.js │ │ └── PageInstanceIdProvider.ts │ │ ├── constants.js │ │ ├── constants.ts │ │ ├── index.js │ │ ├── index.ts │ │ ├── ubi.js │ │ └── ubi.ts ├── ubi-types-js │ ├── index.ts │ ├── index.js │ └── src │ │ └── ubiTypes.js ├── ubi-sdk-music-car-queue-carthingos │ └── index.js ├── ubi-sdk-music-car-presets-carthingos │ └── index.js ├── ubi-sdk-music-car-night-mode-carthingos │ └── index.js ├── ubi-sdk-music-car-phone-call-carthingos │ └── index.js ├── ubi-sdk-music-car-settings-carthingos │ └── index.js ├── ubi-sdk-music-car-track-view-carthingos │ └── index.js ├── ubi-sdk-music-car-voice-view-carthingos │ └── index.js ├── ubi-sdk-music-car-content-shelf-carthingos │ └── index.js ├── ubi-sdk-music-car-pin-pairing-carthingos │ └── index.js ├── ubi-sdk-music-car-lets-drive-modal-carthingos │ └── index.js ├── ubi-sdk-music-car-now-playing-view-carthingos │ └── index.js ├── ubi-sdk-music-car-need-premium-modal-carthingos │ └── index.js ├── ubi-sdk-music-car-onboarding-start-carthingos │ └── index.js ├── ubi-sdk-music-car-podcast-speed-view-carthingos │ └── index.js ├── ubi-sdk-music-car-setup-critical-ota-carthingos │ └── index.js ├── ubi-sdk-music-car-settings-power-modal-carthingos │ └── index.js ├── ubi-sdk-music-car-no-bluetooth-connection-carthingos │ └── index.js ├── ubi-sdk-music-car-non-critical-ota-modal-carthingos │ └── index.js ├── ubi-sdk-music-car-onboarding-learn-voice-carthingos │ └── index.js ├── ubi-sdk-music-car-onboarding-learn-tactile-carthingos │ └── index.js ├── ubi-sdk-music-car-now-playing-view-other-media-carthingos │ └── index.js ├── event-definitions │ └── src │ │ └── events │ │ ├── createUbiExpr2PageView.js │ │ ├── createUbiProd1Impression.js │ │ ├── createUbiProd1Interaction.js │ │ ├── createUbiExpr5ImpressionNonAuth.js │ │ └── createUbiExpr6InteractionNonAuth.js └── encore-foundation │ ├── index.js │ └── desktop │ └── index.js ├── component ├── Presets │ ├── SavingPresetFailed.module.scss │ ├── PresetCard │ │ ├── PresetUnavailable.module.scss │ │ ├── PresetPlaceholder.module.scss │ │ ├── PresetContent.module.scss │ │ ├── PresetPlaceholder.tsx │ │ └── PresetCard.module.scss │ ├── Presets.module.scss │ ├── SavingPresetFailed.tsx │ └── PresetIndicator │ │ ├── PresetNumberIndicator.module.scss │ │ └── PresetNumberIndicator.tsx ├── CarthingUIComponents │ ├── SpotifyLogo │ │ ├── SpotifyLogo.scss │ │ └── SpotifyLogo.tsx │ ├── Icons │ │ ├── IconHome32.tsx │ │ ├── IconPlay48.tsx │ │ ├── IconCheck32.tsx │ │ ├── IconGears64.tsx │ │ ├── IconHeart48.tsx │ │ ├── IconMute32.tsx │ │ ├── IconPause48.tsx │ │ ├── IconAddAlt48.tsx │ │ ├── IconMicOff32.tsx │ │ ├── IconMicOff64.tsx │ │ ├── IconRepeat32.tsx │ │ ├── IconSearch32.tsx │ │ ├── IconVolume48.tsx │ │ ├── IconBlock.tsx │ │ ├── IconMicOn64.tsx │ │ ├── IconShuffle48.tsx │ │ ├── IconInfo32.tsx │ │ ├── IconInfo64.tsx │ │ ├── IconLibrary32.tsx │ │ ├── IconMobile64.tsx │ │ ├── IconSkipBack48.tsx │ │ ├── IconVolumeOff48.tsx │ │ ├── IconRepeatOne32.tsx │ │ ├── IconHomeActive32.tsx │ │ ├── IconSeek15Back48.tsx │ │ ├── IconHeartActive48.tsx │ │ ├── IconSkipForward48.tsx │ │ ├── IconChevronRight48.tsx │ │ ├── IconSeek15Foward48.tsx │ │ ├── IconPlaybackSpeed1X48.tsx │ │ ├── IconPlaybackSpeed2X48.tsx │ │ ├── IconPlaybackSpeed3X48.tsx │ │ ├── IconShuffleActive48.tsx │ │ ├── IconPlaybackSpeed0Point8X48.tsx │ │ ├── IconPlaybackSpeed1Point2X48.tsx │ │ ├── IconPlaybackSpeed0Point5X48.tsx │ │ ├── IconPlaybackSpeed1Point5X48.tsx │ │ ├── IconPlaybackSpeed1Point8X48.tsx │ │ ├── IconPlaybackSpeed2Point5X48.tsx │ │ ├── IconPlaybackSpeed3Point5X48.tsx │ │ ├── IconPublic.tsx │ │ ├── IconSearchActive.tsx │ │ ├── IconLibraryActive.tsx │ │ ├── IconRepeatOne.tsx │ │ ├── IconCheckAlt.tsx │ │ ├── IconMicOff.tsx │ │ ├── IconRepeat.tsx │ │ ├── IconShuffle.tsx │ │ ├── IconPlaybackSpeed1x.tsx │ │ ├── IconPlaybackSpeed1point2x.tsx │ │ ├── IconPlaybackSpeed1point5x.tsx │ │ ├── IconShuffleActive.tsx │ │ ├── IconPhoneAnswer.tsx │ │ └── IconPhoneDecline.tsx │ ├── SpotifySplash │ │ ├── SpotifySplash.module.scss │ │ └── SpotifySplash.tsx │ ├── NowPlaying │ │ ├── NowPlaying.module.scss │ │ └── NowPlaying.tsx │ ├── Trailer │ │ ├── Trailer.tsx │ │ └── Trailer.module.scss │ ├── ButtonGroup │ │ ├── ButtonGroup.module.scss │ │ └── ButtonGroup.tsx │ ├── Spinner │ │ ├── Spinner.module.scss │ │ └── Spinner.tsx │ ├── AppendEllipsis │ │ ├── AppendEllipsis.tsx │ │ └── AppendEllipsis.module.scss │ ├── Button │ │ └── Button.module.scss │ └── Type │ │ └── Type.tsx ├── Settings │ ├── Submenu │ │ ├── Submenu.module.scss │ │ ├── SubmenuHeader.module.scss │ │ ├── SubmenuHeader.tsx │ │ └── SubmenuItem.module.scss │ ├── PhoneCalls │ │ └── PhoneCalls.module.scss │ ├── Settings.module.scss │ ├── PowerTutorial │ │ ├── PowerTutorial.module.scss │ │ └── PowerTutorial.tsx │ ├── MainMenu │ │ └── MainMenu.module.scss │ ├── RestartConfirm │ │ ├── RestartConfirm.module.scss │ │ └── RestartConfirm.tsx │ ├── FactoryReset │ │ └── FactoryReset.module.scss │ ├── Licenses │ │ └── Licenses.module.scss │ ├── UnavailableSettingBanner │ │ └── UnavailableSettingBanner.tsx │ └── DisplayAndBrightness │ │ └── DisplayAndBrightness.module.scss ├── VoiceConfirmation │ └── VoiceConfirmation.module.scss ├── Shelf │ ├── Shelf.scss │ ├── ShelfItem │ │ ├── MoreButton.module.scss │ │ └── InlineTipItem.module.scss │ ├── ShelfSwiper │ │ └── ShelfSwiper.module.scss │ ├── ShelfHeader │ │ ├── ShelfHeader.module.scss │ │ └── ShelfHeaderItem.module.scss │ └── VoiceMutedbanner │ │ └── VoiceMutedBanner.tsx ├── Npv │ ├── Scrubbing │ │ ├── Scrubbing.module.scss │ │ ├── ScrubbingBar.module.scss │ │ ├── ScrubbingBackdrop.module.scss │ │ ├── Scrubbing.tsx │ │ ├── ScrubbingBackdrop.tsx │ │ └── ScrubbingBar.tsx │ ├── ControlButtons │ │ ├── Spacer.tsx │ │ ├── Block.tsx │ │ ├── Controls.module.scss │ │ ├── SaveEpisode.tsx │ │ ├── LikeTrack.tsx │ │ ├── PlayPause.tsx │ │ └── ControlButton.tsx │ ├── OtherMedia │ │ ├── OtherMedia.module.scss │ │ ├── BackToSpotify │ │ │ ├── BackToSpotify.module.scss │ │ │ └── BackToSpotify.tsx │ │ ├── OtherMediaUiState.ts │ │ ├── OtherMedia.tsx │ │ └── Widget │ │ │ └── Widget.module.scss │ ├── PlayingInfo │ │ ├── StatusIcons.module.scss │ │ ├── Artwork.module.scss │ │ ├── PlayingInfoHeader.module.scss │ │ ├── PlayingInfoHeader.tsx │ │ ├── PlayingInfoTitles │ │ │ └── PlayingInfoTitles.module.scss │ │ ├── PlayingInfo.module.scss │ │ └── StatusIcons.tsx │ ├── Volume │ │ ├── VolumeBar.module.scss │ │ ├── Volume.module.scss │ │ ├── VolumeBar.tsx │ │ └── Volume.tsx │ ├── PodcastSpeedOptions │ │ ├── PodcastSpeedItem.module.scss │ │ └── PodcastSpeedOptions.module.scss │ ├── Tips │ │ └── Tips.module.scss │ ├── Npv.module.scss │ └── PlayingInfoOrTip │ │ └── PlayingInfoOrTip.module.scss ├── SwipeDownHandle │ ├── SwipeDownHandle.module.scss │ ├── SwipeDownHandle.tsx │ └── SwipeDownHandleUiState.ts ├── Onboarding │ ├── Onboarding.module.scss │ ├── DialPressPulse.tsx │ ├── LearnTactile.module.scss │ ├── BackPressBanner.tsx │ ├── NoInteractionModal.module.scss │ ├── DialTurnDots.tsx │ ├── SkipButton.module.scss │ ├── BackPressBanner.module.scss │ ├── DialPressPulse.module.scss │ ├── Start.module.scss │ └── DialTurnDots.module.scss ├── Queue │ ├── QueueEmptyState │ │ ├── EmptyQueueState.module.scss │ │ └── EmptyQueueState.tsx │ ├── QueueSwiper │ │ └── QueueSwiper.module.scss │ ├── Queue.module.scss │ ├── QueueHeader │ │ ├── QueueHeader.module.scss │ │ └── QueueHeader.tsx │ └── QueueListItem │ │ └── QueueListItem.module.scss ├── Overlays │ └── Overlays.module.scss ├── AmbientBackdrop │ ├── AmbientBackdrop.module.scss │ └── AmbientBackdrop.tsx ├── Tracklist │ ├── TracklistHeaderActions.module.scss │ ├── ProgressBar.module.scss │ ├── TracklistHeader.module.scss │ ├── ActionConfirmation │ │ └── QueueConfirmationBanner.tsx │ ├── EmptyTracklistState.tsx │ └── Tracklist.module.scss ├── PhoneCall │ ├── AnswerButton.module.scss │ ├── PhoneCallTimer.module.scss │ ├── DeclineButton.module.scss │ ├── PhoneCall.module.scss │ ├── AnswerButton.tsx │ └── PhoneCallTimer.tsx ├── Setup │ ├── Failed.module.scss │ ├── Connected.module.scss │ ├── Waiting.module.scss │ ├── Waiting.tsx │ ├── BTPairing.module.scss │ ├── Updating.module.scss │ ├── SetupHelp.module.scss │ ├── Connected.tsx │ ├── Failed.tsx │ ├── Welcome.tsx │ ├── Setup.tsx │ ├── BTPairing.tsx │ ├── StartSetup.module.scss │ └── Welcome.module.scss ├── LazyImage │ ├── Placeholder │ │ └── Placeholder.module.scss │ └── LazyImage.module.scss ├── Modals │ ├── NonSupportedType.module.scss │ ├── LegacyModal.module.scss │ ├── LoginRequired.tsx │ ├── NoNetwork.module.scss │ ├── NonSupportedType.tsx │ ├── BluetoothPairing.tsx │ ├── ModalContent.module.scss │ ├── PremiumAccountRequired.tsx │ └── Modal.module.scss ├── CountUpTimer │ └── CountUpTimer.tsx ├── NightMode │ ├── NightModeUbiLogger.ts │ └── NightModeUiState.ts ├── OtaUpdating │ ├── OtaUpdating.module.scss │ └── OtaUpdating.tsx ├── Views │ └── Views.module.scss ├── Main.tsx ├── Promo │ └── Promo.module.scss └── Listening │ └── Listening.module.scss ├── global.d.ts ├── public ├── images │ ├── appstart.png │ ├── progress-icon.png │ ├── explicit.svg │ ├── bluetooth-icon.svg │ ├── menu-dots.svg │ ├── round-corners.svg │ ├── mobile-signal.svg │ └── no-connection.svg ├── static │ └── media │ │ └── other-media.83237a6ee7e62a4fb53b.png └── license │ └── i18n-license.txt ├── fonts ├── CircularSpUIv3T-Bold.woff2 ├── CircularSpUIv3T-Book.woff2 └── CircularSpUIv3T-Black.woff2 ├── .idea ├── watcherTasks.xml ├── vcs.xml ├── .gitignore ├── jsLibraryMappings.xml ├── discord.xml ├── modules.xml └── localhost.iml ├── tsconfig.node.json ├── backtrace_uuid.js ├── push.bat ├── middleware ├── SeedableStorageInterface.ts └── InterappError.ts ├── push.sh ├── helpers ├── PointerListeners.ts ├── Retry.ts ├── TextUtil.ts ├── Pagination.ts └── contentIdExtractor.ts ├── eventhandler ├── ErrorHandlerFilters.ts ├── index.ts └── HardwareEventHandler.ts ├── style └── Variables.js ├── store └── TracklistStore.ts ├── Fonts.css ├── context └── store.tsx ├── .gitignore ├── tsconfig.json ├── App.scss ├── hocs └── withStore.tsx ├── vite.config.ts ├── index.html ├── .github └── workflows │ └── ci.yml └── Main.tsx /types/messages/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./voice"; -------------------------------------------------------------------------------- /@spotify-internal/uri/src/uri_typedefs.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /@spotify-internal/encore-web/types/src/core/components/Icon/Svg.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /@spotify-internal/ubi-logger-js/src/providers/PlaybackIdProvider.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /@spotify-internal/ubi-logger-js/src/constants.js: -------------------------------------------------------------------------------- 1 | export const EMPTY_STRING = ''; 2 | -------------------------------------------------------------------------------- /@spotify-internal/ubi-logger-js/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const EMPTY_STRING = ''; 2 | -------------------------------------------------------------------------------- /@spotify-internal/ubi-logger-js/src/providers/PlayContextUriProvider.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /component/Presets/SavingPresetFailed.module.scss: -------------------------------------------------------------------------------- 1 | .icon { 2 | margin-bottom: 10px; 3 | } 4 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.scss'; 2 | declare module "@spotify-internal/encore-foundation"; -------------------------------------------------------------------------------- /@spotify-internal/encore-web/types/src/core/components/Icon/Svg.ts: -------------------------------------------------------------------------------- 1 | export type IconSize = number; -------------------------------------------------------------------------------- /@spotify-internal/ubi-types-js/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./src/pageIdentifiers"; 2 | export * from "./src/ubiTypes"; -------------------------------------------------------------------------------- /@spotify-internal/ubi-sdk-music-car-queue-carthingos/index.js: -------------------------------------------------------------------------------- 1 | export * from "./CarQueueCarthingosEventFactory.js"; 2 | -------------------------------------------------------------------------------- /@spotify-internal/ubi-types-js/index.js: -------------------------------------------------------------------------------- 1 | export * from "./src/pageIdentifiers"; 2 | export * from "./src/ubiTypes"; 3 | -------------------------------------------------------------------------------- /@spotify-internal/ubi-sdk-music-car-presets-carthingos/index.js: -------------------------------------------------------------------------------- 1 | export * from "./CarPresetsCarthingosEventFactory.js"; 2 | -------------------------------------------------------------------------------- /@spotify-internal/ubi-sdk-music-car-night-mode-carthingos/index.js: -------------------------------------------------------------------------------- 1 | export * from "./CarNightModeCarthingosEventFactory.js"; 2 | -------------------------------------------------------------------------------- /@spotify-internal/ubi-sdk-music-car-phone-call-carthingos/index.js: -------------------------------------------------------------------------------- 1 | export * from "./CarPhoneCallCarthingosEventFactory.js"; 2 | -------------------------------------------------------------------------------- /@spotify-internal/ubi-sdk-music-car-settings-carthingos/index.js: -------------------------------------------------------------------------------- 1 | export * from "./CarSettingsCarthingosEventFactory.js"; 2 | -------------------------------------------------------------------------------- /@spotify-internal/ubi-sdk-music-car-track-view-carthingos/index.js: -------------------------------------------------------------------------------- 1 | export * from "./CarTrackViewCarthingosEventFactory.js"; 2 | -------------------------------------------------------------------------------- /@spotify-internal/ubi-sdk-music-car-voice-view-carthingos/index.js: -------------------------------------------------------------------------------- 1 | export * from "./CarVoiceViewCarthingosEventFactory.js"; 2 | -------------------------------------------------------------------------------- /@spotify-internal/ubi-logger-js/src/index.js: -------------------------------------------------------------------------------- 1 | export { UBI } from './ubi'; 2 | export { UBILogger, } from './loggers/UBILogger'; 3 | -------------------------------------------------------------------------------- /@spotify-internal/ubi-sdk-music-car-content-shelf-carthingos/index.js: -------------------------------------------------------------------------------- 1 | export * from "./CarContentShelfCarthingosEventFactory.js"; 2 | -------------------------------------------------------------------------------- /@spotify-internal/ubi-sdk-music-car-pin-pairing-carthingos/index.js: -------------------------------------------------------------------------------- 1 | export * from "./CarPinPairingCarthingosEventFactory.js"; 2 | -------------------------------------------------------------------------------- /public/images/appstart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NekoLines/Spotify-Car-Thing-Chinese-Webapps/HEAD/public/images/appstart.png -------------------------------------------------------------------------------- /@spotify-internal/ubi-sdk-music-car-lets-drive-modal-carthingos/index.js: -------------------------------------------------------------------------------- 1 | export * from "./CarLetsDriveModalCarthingosEventFactory.js"; 2 | -------------------------------------------------------------------------------- /@spotify-internal/ubi-sdk-music-car-now-playing-view-carthingos/index.js: -------------------------------------------------------------------------------- 1 | export * from "./CarNowPlayingViewCarthingosEventFactory.js"; 2 | -------------------------------------------------------------------------------- /@spotify-internal/ubi-sdk-music-car-need-premium-modal-carthingos/index.js: -------------------------------------------------------------------------------- 1 | export * from "./CarNeedPremiumModalCarthingosEventFactory.js"; 2 | -------------------------------------------------------------------------------- /@spotify-internal/ubi-sdk-music-car-onboarding-start-carthingos/index.js: -------------------------------------------------------------------------------- 1 | export * from "./CarOnboardingStartCarthingosEventFactory.js"; 2 | -------------------------------------------------------------------------------- /@spotify-internal/ubi-sdk-music-car-podcast-speed-view-carthingos/index.js: -------------------------------------------------------------------------------- 1 | export * from "./CarPodcastSpeedViewCarthingosEventFactory.js"; 2 | -------------------------------------------------------------------------------- /@spotify-internal/ubi-sdk-music-car-setup-critical-ota-carthingos/index.js: -------------------------------------------------------------------------------- 1 | export * from "./CarSetupCriticalOtaCarthingosEventFactory.js"; 2 | -------------------------------------------------------------------------------- /fonts/CircularSpUIv3T-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NekoLines/Spotify-Car-Thing-Chinese-Webapps/HEAD/fonts/CircularSpUIv3T-Bold.woff2 -------------------------------------------------------------------------------- /fonts/CircularSpUIv3T-Book.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NekoLines/Spotify-Car-Thing-Chinese-Webapps/HEAD/fonts/CircularSpUIv3T-Book.woff2 -------------------------------------------------------------------------------- /public/images/progress-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NekoLines/Spotify-Car-Thing-Chinese-Webapps/HEAD/public/images/progress-icon.png -------------------------------------------------------------------------------- /@spotify-internal/ubi-sdk-music-car-settings-power-modal-carthingos/index.js: -------------------------------------------------------------------------------- 1 | export * from "./CarSettingsPowerModalCarthingosEventFactory.js"; 2 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/SpotifyLogo/SpotifyLogo.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .white.white { 4 | fill: $white; 5 | } 6 | -------------------------------------------------------------------------------- /fonts/CircularSpUIv3T-Black.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NekoLines/Spotify-Car-Thing-Chinese-Webapps/HEAD/fonts/CircularSpUIv3T-Black.woff2 -------------------------------------------------------------------------------- /@spotify-internal/ubi-logger-js/src/providers/PlaybackIdProvider.ts: -------------------------------------------------------------------------------- 1 | export interface PlaybackIdProvider { 2 | getPlaybackId(): string | null; 3 | } -------------------------------------------------------------------------------- /@spotify-internal/ubi-sdk-music-car-no-bluetooth-connection-carthingos/index.js: -------------------------------------------------------------------------------- 1 | export * from "./CarNoBluetoothConnectionCarthingosEventFactory.js"; 2 | -------------------------------------------------------------------------------- /@spotify-internal/ubi-sdk-music-car-non-critical-ota-modal-carthingos/index.js: -------------------------------------------------------------------------------- 1 | export * from "./CarNonCriticalOtaModalCarthingosEventFactory.js"; 2 | -------------------------------------------------------------------------------- /@spotify-internal/ubi-sdk-music-car-onboarding-learn-voice-carthingos/index.js: -------------------------------------------------------------------------------- 1 | export * from "./CarOnboardingLearnVoiceCarthingosEventFactory.js"; 2 | -------------------------------------------------------------------------------- /@spotify-internal/ubi-sdk-music-car-onboarding-learn-tactile-carthingos/index.js: -------------------------------------------------------------------------------- 1 | export * from "./CarOnboardingLearnTactileCarthingosEventFactory.js"; 2 | -------------------------------------------------------------------------------- /@spotify-internal/ubi-logger-js/src/providers/PlayContextUriProvider.ts: -------------------------------------------------------------------------------- 1 | export interface PlayContextUriProvider { 2 | getPlayContextUri(): string | null; 3 | } -------------------------------------------------------------------------------- /@spotify-internal/ubi-sdk-music-car-now-playing-view-other-media-carthingos/index.js: -------------------------------------------------------------------------------- 1 | export * from "./CarNowPlayingViewOtherMediaCarthingosEventFactory.js"; 2 | -------------------------------------------------------------------------------- /@spotify-internal/uri/src/enums/uri_format.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The format for the URI to parse. 3 | */ 4 | export enum URIFormat { 5 | URI = 0, 6 | URL = 1, 7 | } 8 | -------------------------------------------------------------------------------- /component/Settings/Submenu/Submenu.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .submenu { 4 | width: $device-width; 5 | height: $device-height; 6 | } 7 | -------------------------------------------------------------------------------- /.idea/watcherTasks.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /component/VoiceConfirmation/VoiceConfirmation.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .confirmationIcon { 4 | display: block; 5 | color: $white; 6 | } 7 | -------------------------------------------------------------------------------- /public/static/media/other-media.83237a6ee7e62a4fb53b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NekoLines/Spotify-Car-Thing-Chinese-Webapps/HEAD/public/static/media/other-media.83237a6ee7e62a4fb53b.png -------------------------------------------------------------------------------- /@spotify-internal/ubi-logger-js/src/index.ts: -------------------------------------------------------------------------------- 1 | export { UBI } from './ubi'; 2 | export { 3 | type EventSender, 4 | type EventSenderOptions, 5 | UBILogger, 6 | } from './loggers/UBILogger'; 7 | -------------------------------------------------------------------------------- /component/Shelf/Shelf.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | #shelf { 4 | width: $device-width; 5 | height: $device-height; 6 | overflow: hidden; 7 | border-radius: 0; 8 | } 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconHome32.tsx: -------------------------------------------------------------------------------- 1 | import { IconHome } from '@spotify-internal/encore-web'; 2 | 3 | const IconHome32 = () => ; 4 | 5 | export default IconHome32; 6 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconPlay48.tsx: -------------------------------------------------------------------------------- 1 | import { IconPlay } from '@spotify-internal/encore-web'; 2 | 3 | const IconPlay48 = () => ; 4 | 5 | export default IconPlay48; 6 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconCheck32.tsx: -------------------------------------------------------------------------------- 1 | import { IconCheck } from '@spotify-internal/encore-web'; 2 | 3 | const IconCheck32 = () => ; 4 | 5 | export default IconCheck32; 6 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconGears64.tsx: -------------------------------------------------------------------------------- 1 | import { IconGears } from '@spotify-internal/encore-web'; 2 | 3 | const IconGears64 = () => ; 4 | 5 | export default IconGears64; 6 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconHeart48.tsx: -------------------------------------------------------------------------------- 1 | import { IconHeart } from '@spotify-internal/encore-web'; 2 | 3 | const IconHeart48 = () => ; 4 | 5 | export default IconHeart48; 6 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconMute32.tsx: -------------------------------------------------------------------------------- 1 | import { IconMicOff } from '@spotify-internal/encore-web'; 2 | 3 | const IconMute32 = () => ; 4 | 5 | export default IconMute32; 6 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconPause48.tsx: -------------------------------------------------------------------------------- 1 | import { IconPause } from '@spotify-internal/encore-web'; 2 | 3 | const IconPause48 = () => ; 4 | 5 | export default IconPause48; 6 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconAddAlt48.tsx: -------------------------------------------------------------------------------- 1 | import { IconPlusAlt } from '@spotify-internal/encore-web'; 2 | 3 | const IconAddAlt48 = () => ; 4 | 5 | export default IconAddAlt48; 6 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconMicOff32.tsx: -------------------------------------------------------------------------------- 1 | import { IconMicOff } from '@spotify-internal/encore-web'; 2 | 3 | const IconMicOff32 = () => ; 4 | 5 | export default IconMicOff32; 6 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconMicOff64.tsx: -------------------------------------------------------------------------------- 1 | import { IconMicOff } from '@spotify-internal/encore-web'; 2 | 3 | const IconMicOff64 = () => ; 4 | 5 | export default IconMicOff64; 6 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconRepeat32.tsx: -------------------------------------------------------------------------------- 1 | import { IconRepeat } from '@spotify-internal/encore-web'; 2 | 3 | const IconRepeat32 = () => ; 4 | 5 | export default IconRepeat32; 6 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconSearch32.tsx: -------------------------------------------------------------------------------- 1 | import { IconSearch } from '@spotify-internal/encore-web'; 2 | 3 | const IconSearch32 = () => ; 4 | 5 | export default IconSearch32; 6 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconVolume48.tsx: -------------------------------------------------------------------------------- 1 | import { IconVolume } from '@spotify-internal/encore-web'; 2 | 3 | const IconVolume48 = () => ; 4 | 5 | export default IconVolume48; 6 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconBlock.tsx: -------------------------------------------------------------------------------- 1 | import { IconBlock } from '@spotify-internal/encore-web'; 2 | 3 | const CarThingIconBlock = () => ; 4 | 5 | export default CarThingIconBlock; 6 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconMicOn64.tsx: -------------------------------------------------------------------------------- 1 | import { IconMicrophone } from '@spotify-internal/encore-web'; 2 | 3 | const IconMicOn64 = () => ; 4 | 5 | export default IconMicOn64; 6 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconShuffle48.tsx: -------------------------------------------------------------------------------- 1 | import { IconShuffle } from '@spotify-internal/encore-web'; 2 | 3 | const IconShuffle48 = () => ; 4 | 5 | export default IconShuffle48; 6 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconInfo32.tsx: -------------------------------------------------------------------------------- 1 | import { IconInformationAlt } from '@spotify-internal/encore-web'; 2 | 3 | const IconInfo32 = () => ; 4 | 5 | export default IconInfo32; 6 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconInfo64.tsx: -------------------------------------------------------------------------------- 1 | import { IconInformationAlt } from '@spotify-internal/encore-web'; 2 | 3 | const IconInfo64 = () => ; 4 | 5 | export default IconInfo64; 6 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconLibrary32.tsx: -------------------------------------------------------------------------------- 1 | import { IconCollection } from '@spotify-internal/encore-web'; 2 | 3 | const IconLibrary32 = () => ; 4 | 5 | export default IconLibrary32; 6 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconMobile64.tsx: -------------------------------------------------------------------------------- 1 | import { IconDeviceMobile } from '@spotify-internal/encore-web'; 2 | 3 | const IconMobile64 = () => ; 4 | 5 | export default IconMobile64; 6 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconSkipBack48.tsx: -------------------------------------------------------------------------------- 1 | import { IconSkipBack } from '@spotify-internal/encore-web'; 2 | 3 | const IconSkipBack48 = () => ; 4 | 5 | export default IconSkipBack48; 6 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconVolumeOff48.tsx: -------------------------------------------------------------------------------- 1 | import { IconVolumeOff } from '@spotify-internal/encore-web'; 2 | 3 | const IconVolume48 = () => ; 4 | 5 | export default IconVolume48; 6 | -------------------------------------------------------------------------------- /.idea/jsLibraryMappings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconRepeatOne32.tsx: -------------------------------------------------------------------------------- 1 | import { IconRepeatOnce } from '@spotify-internal/encore-web'; 2 | 3 | const IconRepeatOne32 = () => ; 4 | 5 | export default IconRepeatOne32; 6 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/SpotifySplash/SpotifySplash.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .container { 4 | display: flex; 5 | justify-content: center; 6 | height: 100%; 7 | align-items: center; 8 | } 9 | -------------------------------------------------------------------------------- /component/Shelf/ShelfItem/MoreButton.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .moreIcon { 4 | width: 94px; 5 | height: 94px; 6 | border-radius: 50%; 7 | background: $white; 8 | opacity: 0.3; 9 | } 10 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconHomeActive32.tsx: -------------------------------------------------------------------------------- 1 | import { IconHomeActive } from '@spotify-internal/encore-web'; 2 | 3 | const IconHomeActive32 = () => ; 4 | 5 | export default IconHomeActive32; 6 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconSeek15Back48.tsx: -------------------------------------------------------------------------------- 1 | import { IconSkipBack15 } from '@spotify-internal/encore-web'; 2 | 3 | const IconSeek15Back48 = () => ; 4 | 5 | export default IconSeek15Back48; 6 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconHeartActive48.tsx: -------------------------------------------------------------------------------- 1 | import { IconHeartActive } from '@spotify-internal/encore-web'; 2 | 3 | const IconHeartActive48 = () => ; 4 | 5 | export default IconHeartActive48; 6 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconSkipForward48.tsx: -------------------------------------------------------------------------------- 1 | import { IconSkipForward } from '@spotify-internal/encore-web'; 2 | 3 | const IconSkipForward48 = () => ; 4 | 5 | export default IconSkipForward48; 6 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconChevronRight48.tsx: -------------------------------------------------------------------------------- 1 | import { IconChevronRight } from '@spotify-internal/encore-web'; 2 | 3 | const IconChevronRight48 = () => ; 4 | 5 | export default IconChevronRight48; 6 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconSeek15Foward48.tsx: -------------------------------------------------------------------------------- 1 | import { IconSkipForward15 } from '@spotify-internal/encore-web'; 2 | 3 | const IconSeek15Forward48 = () => ; 4 | 5 | export default IconSeek15Forward48; 6 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/NowPlaying/NowPlaying.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .nowPlayingWrapper { 4 | display: flex; 5 | align-items: baseline; 6 | } 7 | 8 | .nowPlayingText { 9 | margin-left: 12px; 10 | } 11 | -------------------------------------------------------------------------------- /component/Npv/Scrubbing/Scrubbing.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .scrubbingClickArea { 4 | z-index: $scrubber-z-index; 5 | position: absolute; 6 | top: 290px; 7 | height: 72px; 8 | width: $device-width; 9 | } 10 | -------------------------------------------------------------------------------- /component/SwipeDownHandle/SwipeDownHandle.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .swipeDownHandle { 4 | position: absolute; 5 | top: 0; 6 | height: 10px; 7 | width: $device-width; 8 | z-index: $overlay-z-index; 9 | } 10 | -------------------------------------------------------------------------------- /.idea/discord.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /@spotify-internal/uri/src/enums/uri_particle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The various URI Particles. 3 | */ 4 | export const URIParticle = { 5 | APP: 'app', 6 | FACEBOOK: 'facebook', 7 | GLOBAL: 'global', 8 | TOP: 'top', 9 | USER: 'user', 10 | }; 11 | -------------------------------------------------------------------------------- /@spotify-internal/uri/src/enums/uri_particle.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The various URI Particles. 3 | */ 4 | export const URIParticle = { 5 | APP: 'app', 6 | FACEBOOK: 'facebook', 7 | GLOBAL: 'global', 8 | TOP: 'top', 9 | USER: 'user', 10 | } as const; 11 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconPlaybackSpeed1X48.tsx: -------------------------------------------------------------------------------- 1 | import { IconPlaybackSpeed1x } from '@spotify-internal/encore-web'; 2 | 3 | const IconPlaybackSpeed1X48 = () => ; 4 | 5 | export default IconPlaybackSpeed1X48; 6 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconPlaybackSpeed2X48.tsx: -------------------------------------------------------------------------------- 1 | import { IconPlaybackSpeed2x } from '@spotify-internal/encore-web'; 2 | 3 | const IconPlaybackSpeed2X48 = () => ; 4 | 5 | export default IconPlaybackSpeed2X48; 6 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconPlaybackSpeed3X48.tsx: -------------------------------------------------------------------------------- 1 | import { IconPlaybackSpeed3x } from '@spotify-internal/encore-web'; 2 | 3 | const IconPlaybackSpeed3X48 = () => ; 4 | 5 | export default IconPlaybackSpeed3X48; 6 | -------------------------------------------------------------------------------- /public/images/explicit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /component/Settings/PhoneCalls/PhoneCalls.module.scss: -------------------------------------------------------------------------------- 1 | .scrollContainer { 2 | height: 352px; 3 | overflow: scroll; 4 | } 5 | 6 | .text { 7 | padding: 40px; 8 | } 9 | 10 | .submenuItemWrapper { 11 | margin-top: 8px; 12 | margin-bottom: 8px; 13 | } 14 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconShuffleActive48.tsx: -------------------------------------------------------------------------------- 1 | import IconShuffleActive from 'component/CarthingUIComponents/Icons/IconShuffleActive'; 2 | 3 | const IconShuffleActive48 = () => ; 4 | 5 | export default IconShuffleActive48; 6 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Trailer/Trailer.tsx: -------------------------------------------------------------------------------- 1 | import styles from './Trailer.module.scss'; 2 | 3 | const Trailer = () => ( 4 |
5 |
TRAILER
6 |
7 | ); 8 | 9 | export default Trailer; 10 | -------------------------------------------------------------------------------- /component/Npv/ControlButtons/Spacer.tsx: -------------------------------------------------------------------------------- 1 | import ControlButton from 'component/Npv/ControlButtons/ControlButton'; 2 | import { NpvIcon } from 'component/Npv/ControlButtons/Controls'; 3 | 4 | const Spacer = () => ; 5 | 6 | export default Spacer; 7 | -------------------------------------------------------------------------------- /component/Presets/PresetCard/PresetUnavailable.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | $preset-card-width: 168px; 4 | 5 | .presetUnavailableTitles { 6 | margin-top: 24px; 7 | display: flex; 8 | flex-direction: column; 9 | align-items: center; 10 | } 11 | -------------------------------------------------------------------------------- /public/images/bluetooth-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /@spotify-internal/uri/src/enums/uri_format.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The format for the URI to parse. 3 | */ 4 | export var URIFormat; 5 | (function (URIFormat) { 6 | URIFormat[URIFormat["URI"] = 0] = "URI"; 7 | URIFormat[URIFormat["URL"] = 1] = "URL"; 8 | })(URIFormat || (URIFormat = {})); 9 | -------------------------------------------------------------------------------- /@spotify-internal/encore-web/es/components/FormToggle/Checkbox.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { formCheck } from "../../styles"; 3 | export var Checkbox = styled.label.withConfig({ 4 | displayName: "Checkbox", 5 | componentId: "jka46s-0" 6 | })(["", ";"], formCheck()); -------------------------------------------------------------------------------- /@spotify-internal/encore-web/es/styles/mixins/baseline.js: -------------------------------------------------------------------------------- 1 | // 2 | // Baseline style for most components 3 | // 4 | import { css } from 'styled-components'; 5 | export var rootStyle = function rootStyle() { 6 | return css(["box-sizing:border-box;-webkit-tap-highlight-color:transparent;"]); 7 | }; -------------------------------------------------------------------------------- /backtrace_uuid.js: -------------------------------------------------------------------------------- 1 | export const getBacktraceUuid = (chunk) => { 2 | const values = { '/static/js/main.js.map': '1E902F45-1F1F-209F-CE9D-87C2EF9BC129' }; 3 | let uuid = values[chunk]; 4 | if (uuid === undefined) uuid = values[Object.keys(values)[0]]; 5 | return uuid; 6 | }; 7 | -------------------------------------------------------------------------------- /@spotify-internal/encore-web/es/styles/mixins/visuallyHidden.js: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components'; 2 | export var visuallyHidden = function visuallyHidden() { 3 | return css(["border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;"]); 4 | }; -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconPlaybackSpeed0Point8X48.tsx: -------------------------------------------------------------------------------- 1 | import { IconPlaybackSpeed0point8x } from '@spotify-internal/encore-web'; 2 | const IconPlaybackSpeed0Point8X48 = () => ( 3 | 4 | ); 5 | export default IconPlaybackSpeed0Point8X48; 6 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconPlaybackSpeed1Point2X48.tsx: -------------------------------------------------------------------------------- 1 | import { IconPlaybackSpeed1point2x } from '@spotify-internal/encore-web'; 2 | 3 | const IconPlaybackSpeed1Point2X48 = () => ( 4 | 5 | ); 6 | export default IconPlaybackSpeed1Point2X48; 7 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconPlaybackSpeed0Point5X48.tsx: -------------------------------------------------------------------------------- 1 | import { IconPlaybackSpeed0point5x } from '@spotify-internal/encore-web'; 2 | 3 | const IconPlaybackSpeed0Point5X48 = () => ( 4 | 5 | ); 6 | 7 | export default IconPlaybackSpeed0Point5X48; 8 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconPlaybackSpeed1Point5X48.tsx: -------------------------------------------------------------------------------- 1 | import { IconPlaybackSpeed1point5x } from '@spotify-internal/encore-web'; 2 | 3 | const IconPlaybackSpeed1Point5X48 = () => ( 4 | 5 | ); 6 | 7 | export default IconPlaybackSpeed1Point5X48; 8 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconPlaybackSpeed1Point8X48.tsx: -------------------------------------------------------------------------------- 1 | import { IconPlaybackSpeed1Point8x } from '@spotify-internal/encore-web'; 2 | 3 | const IconPlaybackSpeed1Point8X48 = () => ( 4 | 5 | ); 6 | 7 | export default IconPlaybackSpeed1Point8X48; 8 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconPlaybackSpeed2Point5X48.tsx: -------------------------------------------------------------------------------- 1 | import { IconPlaybackSpeed2Point5x } from '@spotify-internal/encore-web'; 2 | 3 | const IconPlaybackSpeed2Point5X48 = () => ( 4 | 5 | ); 6 | 7 | export default IconPlaybackSpeed2Point5X48; 8 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconPlaybackSpeed3Point5X48.tsx: -------------------------------------------------------------------------------- 1 | import { IconPlaybackSpeed3Point5x } from '@spotify-internal/encore-web'; 2 | 3 | const IconPlaybackSpeed3Point5X48 = () => ( 4 | 5 | ); 6 | 7 | export default IconPlaybackSpeed3Point5X48; 8 | -------------------------------------------------------------------------------- /component/Npv/OtherMedia/OtherMedia.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .otherMedia { 4 | background: $black; 5 | height: 336px; 6 | padding: 24px 40px; 7 | } 8 | 9 | .topBar { 10 | display: flex; 11 | justify-content: space-between; 12 | margin-right: 6px; 13 | } 14 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /component/Onboarding/Onboarding.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .animationExit { 4 | opacity: 1; 5 | } 6 | 7 | .animationExitActive { 8 | opacity: 0; 9 | transition: opacity 1300ms $easing-function-ease-out-cubic; 10 | } 11 | 12 | .animationExitDone { 13 | opacity: 0; 14 | } 15 | -------------------------------------------------------------------------------- /component/Queue/QueueEmptyState/EmptyQueueState.module.scss: -------------------------------------------------------------------------------- 1 | .emptyBody { 2 | width: 100%; 3 | height: 330px; 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | } 8 | 9 | .infoText { 10 | width: 560px; 11 | text-align: center; 12 | transform: translateY(-48px); 13 | } 14 | -------------------------------------------------------------------------------- /component/Overlays/Overlays.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .overlay { 4 | height: $device-height; 5 | width: $device-width; 6 | position: absolute; 7 | top: 0; 8 | z-index: $overlay-z-index; 9 | 10 | @include transition-opacity-transform; 11 | @include rounded-corners; 12 | } 13 | -------------------------------------------------------------------------------- /push.bat: -------------------------------------------------------------------------------- 1 | adb shell mount -o remount,rw / 2 | adb shell mv /usr/share/qt-superbird-app/webapp /tmp/webapp-orig 3 | adb shell mv /tmp/webapp-orig /usr/share/qt-superbird-app/ # it's ok if this fails 4 | adb shell rm -r /tmp/webapp-orig 5 | adb push dist /usr/share/qt-superbird-app/webapp 6 | adb shell supervisorctl restart chromium -------------------------------------------------------------------------------- /middleware/SeedableStorageInterface.ts: -------------------------------------------------------------------------------- 1 | export default interface SeedableStorageInterface { 2 | // this file wasn't in the sourcemap unfortunately, we can only speculate what it is 3 | 4 | seeded: any; 5 | setItem: (key: string, value: string) => any; 6 | getItem: (key: string) => string | null; 7 | clear: () => any; 8 | } -------------------------------------------------------------------------------- /push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | adb shell mount -o remount,rw / 4 | adb shell mv /usr/share/qt-superbird-app/webapp /tmp/webapp-orig 5 | adb shell mv /tmp/webapp-orig /usr/share/qt-superbird-app/ # it's ok if this fails 6 | adb shell rm -r /tmp/webapp-orig 7 | adb push dist /usr/share/qt-superbird-app/webapp 8 | adb shell supervisorctl restart chromium -------------------------------------------------------------------------------- /helpers/PointerListeners.ts: -------------------------------------------------------------------------------- 1 | export default (callback: (isPressed: boolean) => void) => { 2 | return { 3 | onTouchStart: () => callback(true), 4 | onMouseDown: () => callback(true), 5 | onTouchCancel: () => callback(false), 6 | onTouchEnd: () => callback(false), 7 | onMouseUp: () => callback(false), 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /component/Queue/QueueSwiper/QueueSwiper.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | $dial-center-diff: 68px; 4 | 5 | .container { 6 | position: relative; 7 | width: 100%; 8 | height: $device-height + $dial-center-diff; 9 | overflow: hidden; 10 | } 11 | 12 | .queueSlideScrolled { 13 | transform: translateY(48px); 14 | } 15 | -------------------------------------------------------------------------------- /component/Settings/Settings.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .settingsLayer { 4 | position: absolute; 5 | width: $device-width; 6 | 7 | @include transition-opacity-transform; 8 | 9 | background-color: $black; 10 | } 11 | 12 | .transparent { 13 | background-color: $opacity-black-90; 14 | z-index: 1; 15 | } 16 | -------------------------------------------------------------------------------- /component/Presets/PresetCard/PresetPlaceholder.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | $preset-card-width: 168px; 4 | 5 | .presetPlaceholder { 6 | text-align: center; 7 | 8 | .title { 9 | width: $preset-card-width; 10 | color: $opacity-white-70; 11 | } 12 | 13 | .active { 14 | color: $white; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /component/AmbientBackdrop/AmbientBackdrop.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .ambientBackdrop { 4 | @include transition-background; 5 | 6 | height: $device-height; 7 | overflow: hidden; 8 | position: absolute; 9 | width: $device-width; 10 | will-change: transform; 11 | z-index: -1; 12 | border-radius: 10px; 13 | } 14 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/ButtonGroup/ButtonGroup.module.scss: -------------------------------------------------------------------------------- 1 | .buttonGroupGrid { 2 | display: grid; 3 | gap: 24px; 4 | width: fit-content; 5 | 6 | // Only supporting 2 horizontal buttons? 🤔 7 | &.horizontal { 8 | grid-template-columns: 1fr 1fr; 9 | gap: 32px; 10 | } 11 | 12 | & > * { 13 | width: 100%; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /component/Tracklist/TracklistHeaderActions.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .actions { 4 | @include transition-transform; 5 | 6 | height: 80px; 7 | width: 100px; 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | 12 | &.smallHeader { 13 | transform: translateY(-12px); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /@spotify-internal/ubi-logger-js/src/providers/PageInstanceIdProvider.js: -------------------------------------------------------------------------------- 1 | export class UBIPageInstanceIdProvider { 2 | _currentPageInstanceId = null; 3 | setPageInstanceId(pageInstanceId) { 4 | this._currentPageInstanceId = pageInstanceId; 5 | } 6 | getPageInstanceId() { 7 | return this._currentPageInstanceId; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /@spotify-internal/uri/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./src/base62"; 2 | export * from "./src/factories"; 3 | export * from "./src/type_guards"; 4 | export * from "./src/uri"; 5 | // export * from "./src/uri_typedefs"; 6 | export * from "./src/enums/prefix"; 7 | export * from "./src/enums/uri_type"; 8 | export * from "./src/enums/uri_format"; 9 | export * from "./src/enums/uri_particle"; -------------------------------------------------------------------------------- /component/CarthingUIComponents/Trailer/Trailer.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .box { 4 | background-color: $gray-70; 5 | padding: 4px; 6 | border-radius: 3px; 7 | margin-right: 8px; 8 | width: fit-content; 9 | } 10 | 11 | .text { 12 | @include text-style(18px, 16px, 0); 13 | 14 | color: $black; 15 | font-weight: bold; 16 | } 17 | -------------------------------------------------------------------------------- /@spotify-internal/encore-web/es/contexts/KeyboardDetectionContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | export var keyboardDetectionContextDefault = { 3 | isUsingKeyboard: true 4 | }; 5 | var KeyboardDetectionContext = /*#__PURE__*/React.createContext(keyboardDetectionContextDefault); 6 | KeyboardDetectionContext.displayName = 'KeyboardDetection'; 7 | export { KeyboardDetectionContext }; -------------------------------------------------------------------------------- /@spotify-internal/uri/index.js: -------------------------------------------------------------------------------- 1 | export * from "./src/base62"; 2 | export * from "./src/factories"; 3 | export * from "./src/type_guards"; 4 | export * from "./src/uri"; 5 | // export * from "./src/uri_typedefs"; 6 | export * from "./src/enums/prefix"; 7 | export * from "./src/enums/uri_type"; 8 | export * from "./src/enums/uri_format"; 9 | export * from "./src/enums/uri_particle"; 10 | -------------------------------------------------------------------------------- /component/Npv/OtherMedia/BackToSpotify/BackToSpotify.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .backToSpotify { 4 | background-color: $gray-20; 5 | width: fit-content; 6 | padding: 8px 16px 8px 12px; 7 | border-radius: 50px; 8 | display: flex; 9 | 10 | &.pressed { 11 | opacity: 0.5; 12 | } 13 | } 14 | 15 | .label { 16 | padding-left: 8px; 17 | } 18 | -------------------------------------------------------------------------------- /component/Npv/PlayingInfo/StatusIcons.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .statusIcons { 4 | top: 40px; 5 | right: 40px; 6 | align-items: center; 7 | flex-wrap: nowrap; 8 | justify-content: space-between; 9 | display: flex; 10 | color: $white; 11 | opacity: 0.7; 12 | 13 | > div:not(:first-child) { 14 | margin-left: 32px; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /component/Queue/Queue.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | $small-header-diff: 48px; 4 | 5 | .queue { 6 | @include transition-transform; 7 | 8 | width: $device-width; 9 | height: $device-height + $small-header-diff; 10 | position: relative; 11 | overflow: hidden; 12 | 13 | &.smallHeader { 14 | transform: translateY(-$small-header-diff); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /@spotify-internal/encore-web/es/components/Icon/deprecated-encore-web/IconMicrophone/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IconMic } from "../../icons/IconMic"; 3 | /** 4 | * @deprecated The name IconMicrophone is deprecated and will be archived soon. Use IconMic instead. 5 | */ 6 | 7 | export function IconMicrophone(props) { 8 | return /*#__PURE__*/React.createElement(IconMic, props); 9 | } -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconPublic.tsx: -------------------------------------------------------------------------------- 1 | import { IconPublic } from '@spotify-internal/encore-web'; 2 | import { IconSize } from '@spotify-internal/encore-web/types/src/core/components/Icon/Svg'; 3 | 4 | interface Props { 5 | iconSize: IconSize; 6 | } 7 | 8 | export default function CarThingIconPublic({ iconSize }: Props) { 9 | return ; 10 | } 11 | -------------------------------------------------------------------------------- /component/Onboarding/DialPressPulse.tsx: -------------------------------------------------------------------------------- 1 | import withStore from 'hocs/withStore'; 2 | import styles from './DialPressPulse.module.scss'; 3 | 4 | const DialPressPulse = () => { 5 | return ( 6 |
7 |
8 |
9 | ); 10 | }; 11 | 12 | export default withStore(DialPressPulse); 13 | -------------------------------------------------------------------------------- /eventhandler/ErrorHandlerFilters.ts: -------------------------------------------------------------------------------- 1 | import InterappError from 'middleware/InterappError'; 2 | 3 | export type ErrorFilterFunction = (e: Error) => boolean; 4 | 5 | export const no_wamp_session_destroyed: ErrorFilterFunction = ( 6 | error: Error, 7 | ) => { 8 | return !( 9 | error instanceof InterappError && 10 | error.message.indexOf('WampSession destroyed') >= 0 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /middleware/InterappError.ts: -------------------------------------------------------------------------------- 1 | import { maskUsernames } from 'helpers/SpotifyUriUtil'; 2 | 3 | class InterappError extends Error { 4 | method: string; 5 | args: Object; 6 | 7 | constructor(message: string, method: string, args: Object) { 8 | super(message); 9 | this.method = method; 10 | this.args = maskUsernames(args); 11 | } 12 | } 13 | 14 | export default InterappError; 15 | -------------------------------------------------------------------------------- /@spotify-internal/encore-web/es/contexts/BrowserDefaultFocusStyleContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | export var browserDefaultContextDefault = { 3 | useBrowserDefaultFocusStyle: false 4 | }; 5 | var BrowserDefaultFocusStyleContext = /*#__PURE__*/React.createContext(browserDefaultContextDefault); 6 | BrowserDefaultFocusStyleContext.displayName = 'BrowserDefault'; 7 | export { BrowserDefaultFocusStyleContext }; -------------------------------------------------------------------------------- /component/Shelf/ShelfSwiper/ShelfSwiper.module.scss: -------------------------------------------------------------------------------- 1 | /* stylelint-disable */ 2 | 3 | .container { 4 | overflow: visible !important; // Override swiper default behaviour 5 | 6 | :global { 7 | .swiper-wrapper { 8 | // Push previous slide out of view 9 | .swiper-slide-prev { 10 | transform: translateX(-18px); 11 | } 12 | } 13 | } 14 | } 15 | 16 | /* stylelint-enable */ 17 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconSearchActive.tsx: -------------------------------------------------------------------------------- 1 | import { IconSize } from '@spotify-internal/encore-web/types/src/core/components/Icon/Svg'; 2 | import { IconSearchActive } from '@spotify-internal/encore-web'; 3 | 4 | type Props = { 5 | iconSize: IconSize; 6 | }; 7 | 8 | export default function CarThingIconSearchActive({ iconSize }: Props) { 9 | return ; 10 | } 11 | -------------------------------------------------------------------------------- /component/Npv/Volume/VolumeBar.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .volumeBar { 4 | height: 8px; 5 | width: 480px; 6 | position: relative; 7 | border-radius: 4px; 8 | transform: translateY(2px); 9 | background: $gray-20; 10 | } 11 | 12 | .volumeLevelFill { 13 | display: block; 14 | height: 100%; 15 | border-radius: 4px; 16 | background-color: $white; 17 | overflow: hidden; 18 | } 19 | -------------------------------------------------------------------------------- /@spotify-internal/encore-web/es/components/Icon/deprecated-encore-web/IconUserAltCircle/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IconUserCircle } from "../../icons/IconUserCircle"; 3 | /** 4 | * @deprecated The name IconUserAltCircle is deprecated and will be archived soon. Use IconUserCircle instead. 5 | */ 6 | 7 | export function IconUserAltCircle(props) { 8 | return /*#__PURE__*/React.createElement(IconUserCircle, props); 9 | } -------------------------------------------------------------------------------- /component/Npv/PlayingInfo/Artwork.module.scss: -------------------------------------------------------------------------------- 1 | $artwork-container-width: 248px; 2 | $artwork-container-height: 248px; 3 | 4 | .artworkTransitionGroup { 5 | display: flex; 6 | } 7 | 8 | .transitionContainer { 9 | position: absolute; 10 | } 11 | 12 | .artwork { 13 | width: $artwork-container-width; 14 | height: $artwork-container-height; 15 | box-shadow: 0 16px 32px rgb(0 0 0 / 20%); 16 | border-radius: 16px; 17 | } 18 | -------------------------------------------------------------------------------- /component/Npv/PodcastSpeedOptions/PodcastSpeedItem.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .speedItem { 4 | height: 112px; 5 | display: flex; 6 | justify-content: space-between; 7 | align-items: center; 8 | padding: 0 40px; 9 | 10 | svg { 11 | margin-right: 30px; 12 | } 13 | } 14 | 15 | .activeItem { 16 | background: $opacity-white-10; 17 | } 18 | 19 | .pressed { 20 | opacity: 0.5; 21 | } 22 | -------------------------------------------------------------------------------- /component/PhoneCall/AnswerButton.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .toDecline { 4 | transform: translateX(123.5px); 5 | opacity: 1; 6 | 7 | svg { 8 | transform: rotate(135deg); 9 | margin-top: 10px; 10 | @include transition-transform; 11 | 12 | path { 13 | fill: $negative; 14 | @include transition-fill; 15 | } 16 | } 17 | @include transition-opacity-transform; 18 | } 19 | -------------------------------------------------------------------------------- /component/Settings/PowerTutorial/PowerTutorial.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .powerTutorial { 4 | width: $device-width; 5 | height: $device-height; 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | justify-content: center; 10 | } 11 | 12 | .title { 13 | margin-bottom: 32px; 14 | } 15 | 16 | .description { 17 | text-align: center; 18 | max-width: 560px; 19 | } 20 | -------------------------------------------------------------------------------- /@spotify-internal/encore-web/es/components/Icon/deprecated-encore-web/IconHeartAltActive/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IconHeartActive } from "../../icons/IconHeartActive"; 3 | /** 4 | * @deprecated The name IconHeartAltActive is deprecated and will be archived soon. Use IconHeartActive instead. 5 | */ 6 | 7 | export function IconHeartAltActive(props) { 8 | return /*#__PURE__*/React.createElement(IconHeartActive, props); 9 | } -------------------------------------------------------------------------------- /@spotify-internal/encore-web/es/components/Icon/deprecated-encore-web/IconCheckAltActive/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IconCheckAltFill } from "../../icons/IconCheckAltFill"; 3 | /** 4 | * @deprecated The name IconCheckAltActive is deprecated and will be archived soon. Use IconCheckAltFill instead. 5 | */ 6 | 7 | export function IconCheckAltActive(props) { 8 | return /*#__PURE__*/React.createElement(IconCheckAltFill, props); 9 | } -------------------------------------------------------------------------------- /@spotify-internal/encore-web/es/components/Icon/deprecated-encore-web/IconDownloadAltActive/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IconDownloaded } from "../../icons/IconDownloaded"; 3 | /** 4 | * @deprecated The name IconDownloadAltActive is deprecated and will be archived soon. Use IconDownloaded instead. 5 | */ 6 | 7 | export function IconDownloadAltActive(props) { 8 | return /*#__PURE__*/React.createElement(IconDownloaded, props); 9 | } -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconLibraryActive.tsx: -------------------------------------------------------------------------------- 1 | import { IconSize } from '@spotify-internal/encore-web/types/src/core/components/Icon/Svg'; 2 | import { IconCollectionActive } from '@spotify-internal/encore-web'; 3 | 4 | type Props = { 5 | iconSize: IconSize; 6 | }; 7 | 8 | const IconLibraryActive = ({ iconSize }: Props) => ( 9 | 10 | ); 11 | 12 | export default IconLibraryActive; 13 | -------------------------------------------------------------------------------- /component/Onboarding/LearnTactile.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .learnTactile { 4 | height: $device-height; 5 | width: $device-width; 6 | background-color: transparent; 7 | position: relative; 8 | } 9 | 10 | .blockTouch { 11 | height: $device-height; 12 | width: $device-width; 13 | position: absolute; 14 | top: 0; 15 | z-index: $onboarding-tactile-z-index + 1; 16 | background-color: transparent; 17 | } 18 | -------------------------------------------------------------------------------- /component/Settings/Submenu/SubmenuHeader.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .header { 4 | position: sticky; 5 | height: 144px; 6 | width: 100%; 7 | display: flex; 8 | justify-content: space-between; 9 | align-items: center; 10 | } 11 | 12 | .headerDetails { 13 | margin-left: 40px; 14 | width: 100%; 15 | display: flex; 16 | align-items: center; 17 | 18 | svg { 19 | margin-right: 16px; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /@spotify-internal/encore-web/es/styles/index.js: -------------------------------------------------------------------------------- 1 | export * from "./global-styles.js"; 2 | export * from "./semantic-theme.js"; 3 | export * from "./variables.js"; 4 | export * from "./mixins/buttons.js"; 5 | export * from "./mixins/baseline.js"; 6 | export * from "./mixins/focusBorders.js"; 7 | export * from "./mixins/forms.js"; 8 | export * from "./mixins/localization.js"; 9 | export * from "./mixins/visuallyHidden.js"; 10 | export * from "./mixins/type.js"; -------------------------------------------------------------------------------- /@spotify-internal/encore-web/es/components/Icon/deprecated-encore-web/IconExclamationAlt/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IconExclamationCircle } from "../../icons/IconExclamationCircle"; 3 | /** 4 | * @deprecated The name IconExclamationAlt is deprecated and will be archived soon. Use IconExclamationCircle instead. 5 | */ 6 | 7 | export function IconExclamationAlt(props) { 8 | return /*#__PURE__*/React.createElement(IconExclamationCircle, props); 9 | } -------------------------------------------------------------------------------- /@spotify-internal/encore-web/es/components/LogoSpotify/Svg.js: -------------------------------------------------------------------------------- 1 | import { greenLight } from '@spotify-internal/encore-foundation'; 2 | import styled from 'styled-components'; 3 | import { cssColorValue } from "../../styles"; 4 | export var Svg = styled.svg.withConfig({ 5 | displayName: "Svg", 6 | componentId: "sc-6c3c1v-0" 7 | })(["fill:", ";stroke:transparent;"], function (props) { 8 | return props.useBrandColor ? greenLight : cssColorValue(props.semanticColor); 9 | }); -------------------------------------------------------------------------------- /helpers/Retry.ts: -------------------------------------------------------------------------------- 1 | interface WithSuccess { 2 | success: boolean; 3 | } 4 | 5 | export const tryActionNTimes = async ({ 6 | asyncAction, 7 | n, 8 | }: { 9 | asyncAction: () => Promise; 10 | n: number; 11 | }): Promise => { 12 | let result; 13 | 14 | for (let i = 0; i < n; i++) { 15 | result = await asyncAction(); 16 | if (result.success) { 17 | break; 18 | } 19 | } 20 | 21 | return result; 22 | }; 23 | -------------------------------------------------------------------------------- /component/Presets/Presets.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .presetsBackground { 4 | height: $device-height; 5 | position: relative; 6 | width: $device-width; 7 | background: $black; 8 | } 9 | 10 | .presetIndicatorsWrapper { 11 | display: flex; 12 | justify-content: center; 13 | width: 100%; 14 | height: 85px; 15 | } 16 | 17 | .presetCardsWrapper { 18 | display: flex; 19 | justify-content: center; 20 | width: 100%; 21 | } 22 | -------------------------------------------------------------------------------- /style/Variables.js: -------------------------------------------------------------------------------- 1 | // shim for sass constants 2 | 3 | import variables from './variables.module.scss'; 4 | 5 | export const transitionDurationMs = parseInt( 6 | variables['transition-duration-ms'], 7 | 10, 8 | ); 9 | export const easingFunction = variables['easing-function']; 10 | export const genericEasing = variables['generic-cubic']; 11 | export const recedeDefaultEasing = variables['recede-default-cubic']; 12 | export const greenLight = variables['green-light']; 13 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Spinner/Spinner.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .greenCircle { 4 | stroke-dasharray: 440; 5 | stroke-dashoffset: 1000; 6 | stroke: $green-light; 7 | stroke-linecap: round; 8 | fill: transparent; 9 | transform-origin: 50% 50%; 10 | } 11 | 12 | .spin { 13 | display: flex; 14 | animation: rotate 1s infinite linear; 15 | } 16 | 17 | @keyframes rotate { 18 | to { 19 | transform: rotate(360deg); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /component/Setup/Failed.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .screen { 4 | background-color: $aubergine; 5 | height: $device-height; 6 | width: $device-width; 7 | padding: 32px 48px 40px; 8 | } 9 | 10 | .title { 11 | color: $pink; 12 | margin-bottom: 32px; 13 | 14 | @include text-style(120px, 120px, -0.04em); 15 | } 16 | 17 | .subtitle { 18 | color: $pink; 19 | 20 | @include text-style(36px, 48px, -0.03em); 21 | 22 | max-width: 704px; 23 | } 24 | -------------------------------------------------------------------------------- /component/Tracklist/ProgressBar.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | $progress-backgroud-color: rgb(93 93 93); 4 | 5 | .progressBar { 6 | margin-left: 16px; 7 | height: 8px; 8 | width: 200px; 9 | background-color: $progress-backgroud-color; 10 | clip-path: inset(0 0 0 0 round 4px); 11 | transform: translateY(2px); 12 | } 13 | 14 | .progress { 15 | background-color: $green-light; 16 | clip-path: inset(0 0 0 0 round 4px); 17 | height: 100%; 18 | } 19 | -------------------------------------------------------------------------------- /component/Onboarding/BackPressBanner.tsx: -------------------------------------------------------------------------------- 1 | import withStore from 'hocs/withStore'; 2 | import styles from './BackPressBanner.module.scss'; 3 | import { IconArrowRight } from '@spotify-internal/encore-web'; 4 | 5 | const BackPressBanner = () => { 6 | return ( 7 |
8 | 按下后退键 9 | 10 |
11 | ); 12 | }; 13 | 14 | export default withStore(BackPressBanner); 15 | -------------------------------------------------------------------------------- /component/Onboarding/NoInteractionModal.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .noInteraction { 4 | height: $device-height; 5 | width: $device-width; 6 | position: absolute; 7 | top: 0; 8 | z-index: $onboarding-tactile-z-index + 1; 9 | 10 | @include rounded-corners; 11 | 12 | display: flex; 13 | flex-direction: column; 14 | align-items: center; 15 | text-align: center; 16 | justify-content: center; 17 | background-color: $opacity-black-90; 18 | } 19 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconRepeatOne.tsx: -------------------------------------------------------------------------------- 1 | import { IconRepeatOnce } from '@spotify-internal/encore-web'; 2 | 3 | interface Props { 4 | className?: string; 5 | iconSize: number; 6 | } 7 | 8 | const IconRepeatOne = ({ className, iconSize }: Props) => { 9 | return ( 10 | 16 | ); 17 | }; 18 | 19 | export default IconRepeatOne; 20 | -------------------------------------------------------------------------------- /component/Onboarding/DialTurnDots.tsx: -------------------------------------------------------------------------------- 1 | import withStore from 'hocs/withStore'; 2 | import styles from './DialTurnDots.module.scss'; 3 | 4 | const DialTurnDots = () => { 5 | return ( 6 |
7 |
8 |
9 |
10 |
11 |
12 | ); 13 | }; 14 | 15 | export default withStore(DialTurnDots); 16 | -------------------------------------------------------------------------------- /component/Settings/MainMenu/MainMenu.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .mainMenu { 4 | background-color: $black; 5 | height: $device-height; 6 | } 7 | 8 | .header { 9 | background: $black; 10 | display: flex; 11 | align-items: center; 12 | width: 100%; 13 | height: 144px; 14 | 15 | p { 16 | margin-left: 40px; 17 | padding: 0; 18 | color: $white; 19 | font-weight: bold; 20 | 21 | @include text-style(44px, 49px, -0.02em); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /component/Shelf/ShelfHeader/ShelfHeader.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .shelfTitles { 4 | display: flex; 5 | align-items: center; 6 | margin-top: 28px; 7 | margin-left: 44px; 8 | } 9 | 10 | .titleUnderlineContainer { 11 | margin-top: 4px; 12 | margin-bottom: 40px; 13 | } 14 | 15 | .titleUnderline { 16 | @include transition-transform; 17 | 18 | transform-origin: left; 19 | background-color: $green-light; 20 | height: 4px; 21 | width: 1px; 22 | } 23 | -------------------------------------------------------------------------------- /@spotify-internal/event-definitions/src/events/createUbiExpr2PageView.js: -------------------------------------------------------------------------------- 1 | // NOTE: This code was generated and should not be changed 2 | /** 3 | * A builder for UbiExpr2PageView 4 | * 5 | * @param data - The event data 6 | * @return The formatted event data for UbiExpr2PageViewEvent 7 | */ 8 | export function createUbiExpr2PageView(data) { 9 | return { 10 | name: 'UbiExpr2PageView', 11 | environments: ['device', 'browser', 'desktop'], 12 | data, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconCheckAlt.tsx: -------------------------------------------------------------------------------- 1 | import { IconCheckAlt } from '@spotify-internal/encore-web'; 2 | import { IconSize } from '@spotify-internal/encore-web/es/components/Icon/Svg'; 3 | 4 | interface Props { 5 | className?: string; 6 | iconSize: number; 7 | } 8 | 9 | const IconCheckAltLocal = ({ className, iconSize }: Props) => ; 10 | 11 | export default IconCheckAltLocal; 12 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/SpotifySplash/SpotifySplash.tsx: -------------------------------------------------------------------------------- 1 | import styles from './SpotifySplash.module.scss'; 2 | import SpotifyLogo from '../SpotifyLogo/SpotifyLogo'; 3 | 4 | const SpotifySplash = () => { 5 | return ( 6 |
7 | 13 |
14 | ); 15 | }; 16 | 17 | export default SpotifySplash; 18 | -------------------------------------------------------------------------------- /component/LazyImage/Placeholder/Placeholder.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .placeholder { 4 | height: 100%; 5 | width: 100%; 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | background-color: $gray-20; 10 | background-clip: content-box; 11 | 12 | &.otherMedia { 13 | background-color: $opacity-white-10; 14 | } 15 | } 16 | 17 | .placeholderIcon { 18 | display: flex; 19 | 20 | svg { 21 | color: $gray-50; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /component/Setup/Connected.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .screen { 4 | background: $aubergine; 5 | height: $device-height; 6 | width: $device-width; 7 | padding: 32px 48px 40px; 8 | } 9 | 10 | .title { 11 | color: $pink; 12 | 13 | @include text-style(120px, 120px, -0.04em); 14 | 15 | width: 704px; 16 | } 17 | 18 | .subtitle { 19 | color: $pink; 20 | margin-top: 32px; 21 | 22 | @include text-style(36px, 48px, -0.02em); 23 | 24 | max-width: 700px; 25 | } 26 | -------------------------------------------------------------------------------- /component/Setup/Waiting.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .screen { 4 | background: $aubergine; 5 | height: $device-height; 6 | width: $device-width; 7 | padding: 32px 48px 40px; 8 | } 9 | 10 | .title { 11 | color: $pink; 12 | 13 | @include text-style(120px, 120px, -0.04em); 14 | 15 | width: 704px; 16 | } 17 | 18 | .subtitle { 19 | color: $pink; 20 | margin-top: 32px; 21 | 22 | @include text-style(36px, 48px, -0.02em); 23 | 24 | max-width: 700px; 25 | } 26 | -------------------------------------------------------------------------------- /@spotify-internal/event-definitions/src/events/createUbiProd1Impression.js: -------------------------------------------------------------------------------- 1 | // NOTE: This code was generated and should not be changed 2 | /** 3 | * A builder for UbiProd1Impression 4 | * 5 | * @param data - The event data 6 | * @return The formatted event data for UbiProd1ImpressionEvent 7 | */ 8 | export function createUbiProd1Impression(data) { 9 | return { 10 | name: 'UbiProd1Impression', 11 | environments: ['device', 'browser', 'desktop'], 12 | data, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /component/Modals/NonSupportedType.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .nonSupportedType { 4 | height: 365px; 5 | width: 560px; 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | } 10 | 11 | .icon { 12 | margin-top: 59px; 13 | color: $black; 14 | } 15 | 16 | .text { 17 | margin-top: 24px; 18 | color: $black; 19 | 20 | @include text-style(36px, 40px, -0.02em); 21 | 22 | text-align: center; 23 | width: 420px; 24 | font-weight: bold; 25 | } 26 | -------------------------------------------------------------------------------- /.idea/localhost.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /@spotify-internal/event-definitions/src/events/createUbiProd1Interaction.js: -------------------------------------------------------------------------------- 1 | // NOTE: This code was generated and should not be changed 2 | /** 3 | * A builder for UbiProd1Interaction 4 | * 5 | * @param data - The event data 6 | * @return The formatted event data for UbiProd1InteractionEvent 7 | */ 8 | export function createUbiProd1Interaction(data) { 9 | return { 10 | name: 'UbiProd1Interaction', 11 | environments: ['device', 'browser', 'desktop'], 12 | data, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /component/Tracklist/TracklistHeader.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .headerWrapper { 4 | height: 152px; 5 | width: 100%; 6 | padding-right: 72px; 7 | z-index: $overlay-z-index + 10; 8 | display: flex; 9 | 10 | @include transition-background; 11 | } 12 | 13 | .header { 14 | display: flex; 15 | align-items: center; 16 | justify-content: space-between; 17 | 18 | @include transition-transform; 19 | 20 | &.smallHeader { 21 | transform: translateY(40px); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /store/TracklistStore.ts: -------------------------------------------------------------------------------- 1 | import { RootStore } from './RootStore'; 2 | import TracklistUiState from 'component/Tracklist/TracklistUiState'; 3 | 4 | class TracklistStore { 5 | rootStore: RootStore; 6 | tracklistUiState: TracklistUiState; 7 | 8 | constructor(rootStore: RootStore) { 9 | this.rootStore = rootStore; 10 | 11 | this.tracklistUiState = new TracklistUiState(rootStore); 12 | } 13 | 14 | reset(): void { 15 | this.tracklistUiState.reset(); 16 | } 17 | } 18 | 19 | export default TracklistStore; 20 | -------------------------------------------------------------------------------- /component/Settings/RestartConfirm/RestartConfirm.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .restartConfirm { 4 | width: $device-width; 5 | height: $device-height; 6 | padding: 116px 120px; 7 | display: flex; 8 | flex-direction: column; 9 | align-items: center; 10 | } 11 | 12 | .title { 13 | @include text-style(44px, 48px, -0.02em); 14 | 15 | margin-bottom: 32px; 16 | font-weight: bold; 17 | } 18 | 19 | .description { 20 | @include text-style(32px, 40px, 0); 21 | 22 | margin-bottom: 40px; 23 | } 24 | -------------------------------------------------------------------------------- /@spotify-internal/encore-web/es/components/FormToggle/Indicator.js: -------------------------------------------------------------------------------- 1 | import { spacer12 } from '@spotify-internal/encore-foundation'; 2 | import styled from 'styled-components'; 3 | import { cssColorValue, semanticColors } from "../../styles"; 4 | export var Indicator = styled.span.withConfig({ 5 | displayName: "Indicator", 6 | componentId: "acu4qz-0" 7 | })(["background:", ";border-radius:inherit;block-size:", ";position:absolute;top:2px;inline-size:", ";transition:all 0.1s ease;"], cssColorValue(semanticColors.backgroundBase), spacer12, spacer12); -------------------------------------------------------------------------------- /component/PhoneCall/PhoneCallTimer.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .enter { 4 | opacity: 0; 5 | } 6 | 7 | .enterActive { 8 | opacity: 1; 9 | 10 | @include transition-opacity; 11 | } 12 | 13 | .exit { 14 | opacity: 1; 15 | } 16 | 17 | .exitActive { 18 | opacity: 0; 19 | 20 | @include transition-opacity; 21 | } 22 | 23 | .timer { 24 | width: 100%; 25 | height: 40px; 26 | display: flex; 27 | justify-content: center; 28 | } 29 | 30 | .timerDiv { 31 | width: 90px; 32 | height: 40px; 33 | } 34 | -------------------------------------------------------------------------------- /component/Queue/QueueEmptyState/EmptyQueueState.tsx: -------------------------------------------------------------------------------- 1 | import styles from 'component/Queue/QueueEmptyState/EmptyQueueState.module.scss'; 2 | 3 | import Type from 'component/CarthingUIComponents/Type/Type'; 4 | 5 | const EmptyQueueState = () => { 6 | return ( 7 |
8 | 9 | 点击“添加到队列”图标,添加歌曲或播客集。 10 | 11 |
12 | ); 13 | }; 14 | 15 | export default EmptyQueueState; 16 | -------------------------------------------------------------------------------- /component/Settings/FactoryReset/FactoryReset.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .factoryReset { 4 | width: $device-width; 5 | height: $device-height; 6 | padding: 68px 112px; 7 | display: flex; 8 | flex-direction: column; 9 | justify-content: center; 10 | } 11 | 12 | .description { 13 | @include text-style(32px, 40px, 0); 14 | 15 | margin-bottom: 32px; 16 | text-align: center; 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | justify-content: space-between; 22 | align-items: center; 23 | } 24 | -------------------------------------------------------------------------------- /@spotify-internal/encore-web/es/components/Icon/Svg.js: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | import { cssColorValue } from "../../styles"; 3 | export default styled.svg.withConfig({ 4 | displayName: "Svg", 5 | componentId: "ytk21e-0" 6 | })(["*{vector-effect:non-scaling-stroke;}", " ", ""], function (props) { 7 | return props.autoMirror && css(["[dir='rtl'] &&{transform:scaleX(-1);}"]); 8 | }, function (props) { 9 | return props.iconColor ? css(["fill:", "};"], cssColorValue(props.iconColor)) : css(["fill:currentColor;"]); 10 | }); -------------------------------------------------------------------------------- /component/Npv/PlayingInfo/PlayingInfoHeader.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .header { 4 | display: flex; 5 | justify-content: space-between; 6 | align-items: center; 7 | height: 32px; 8 | min-width: 0; 9 | } 10 | 11 | .contextTitle { 12 | overflow: hidden; 13 | text-overflow: ellipsis; 14 | white-space: nowrap; 15 | font-weight: bold; 16 | color: rgb(255 255 255 / 50%); 17 | padding: 40px 0 25px 10px; 18 | margin: -40px 32px -25px -10px; 19 | 20 | @include text-style(28px, 32px, -0.01em); 21 | } 22 | -------------------------------------------------------------------------------- /component/PhoneCall/DeclineButton.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .enter { 4 | transform: translateX(150px); 5 | opacity: 0; 6 | } 7 | 8 | .enterActive { 9 | transform: translateX(0); 10 | opacity: 1; 11 | @include transition-opacity-transform; 12 | } 13 | 14 | .exit { 15 | transform: translateX(0); 16 | opacity: 1; 17 | } 18 | 19 | .exitActive { 20 | transform: translateX(150px); 21 | opacity: 0; 22 | @include transition-opacity-transform; 23 | } 24 | 25 | .decline { 26 | margin-left: 40px; 27 | } 28 | -------------------------------------------------------------------------------- /@spotify-internal/event-definitions/src/events/createUbiExpr5ImpressionNonAuth.js: -------------------------------------------------------------------------------- 1 | // NOTE: This code was generated and should not be changed 2 | /** 3 | * A builder for UbiExpr5ImpressionNonAuth 4 | * 5 | * @param data - The event data 6 | * @return The formatted event data for UbiExpr5ImpressionNonAuthEvent 7 | */ 8 | export function createUbiExpr5ImpressionNonAuth(data) { 9 | return { 10 | name: 'UbiExpr5ImpressionNonAuth', 11 | environments: ['devicenonauth', 'browsernonauth', 'desktopnonauth'], 12 | data, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /Fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: spotify-circular; 3 | src: url('./fonts/CircularSpUIv3T-Book.woff2') format('woff2'); 4 | font-weight: 400; 5 | font-style: normal; 6 | } 7 | 8 | @font-face { 9 | font-family: spotify-circular; 10 | src: url('./fonts/CircularSpUIv3T-Bold.woff2') format('woff2'); 11 | font-weight: 700; 12 | font-style: normal; 13 | } 14 | 15 | @font-face { 16 | font-family: spotify-circular; 17 | src: url('./fonts/CircularSpUIv3T-Black.woff2') format('woff2'); 18 | font-weight: 900; 19 | font-style: normal; 20 | } 21 | -------------------------------------------------------------------------------- /component/Modals/LegacyModal.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .legacyModal { 4 | z-index: $overlay-z-index; 5 | position: absolute; 6 | display: flex; 7 | width: 800px; 8 | height: 480px; 9 | background-color: $backdrop-overlay; 10 | } 11 | 12 | .content { 13 | position: relative; 14 | margin: auto; 15 | border-radius: 15px; 16 | box-shadow: 0 0 15px 2px black; 17 | background-color: $white; 18 | } 19 | 20 | .icon { 21 | margin-top: -12px; 22 | position: absolute; 23 | right: -74px; 24 | color: $white; 25 | } 26 | -------------------------------------------------------------------------------- /@spotify-internal/event-definitions/src/events/createUbiExpr6InteractionNonAuth.js: -------------------------------------------------------------------------------- 1 | // NOTE: This code was generated and should not be changed 2 | /** 3 | * A builder for UbiExpr6InteractionNonAuth 4 | * 5 | * @param data - The event data 6 | * @return The formatted event data for UbiExpr6InteractionNonAuthEvent 7 | */ 8 | export function createUbiExpr6InteractionNonAuth(data) { 9 | return { 10 | name: 'UbiExpr6InteractionNonAuth', 11 | environments: ['devicenonauth', 'browsernonauth', 'desktopnonauth'], 12 | data, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconMicOff.tsx: -------------------------------------------------------------------------------- 1 | import { IconMicOff } from '@spotify-internal/encore-web'; 2 | import { IconSize } from '@spotify-internal/encore-web/types/src/core/components/Icon/Svg'; 3 | 4 | interface Props { 5 | className?: string; 6 | iconSize: number; 7 | } 8 | 9 | const CarThingIconMicOff = ({ className, iconSize }: Props) => ( 10 | 16 | ); 17 | 18 | export default CarThingIconMicOff; 19 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconRepeat.tsx: -------------------------------------------------------------------------------- 1 | import { IconRepeat } from '@spotify-internal/encore-web'; 2 | import { IconSize } from '@spotify-internal/encore-web/types/src/core/components/Icon/Svg'; 3 | 4 | interface Props { 5 | className?: string; 6 | iconSize: number; 7 | } 8 | 9 | const CarThingIconRepeat = ({ className, iconSize }: Props) => ( 10 | 16 | ); 17 | 18 | export default CarThingIconRepeat; 19 | -------------------------------------------------------------------------------- /component/Settings/Licenses/Licenses.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .licenses { 4 | width: $device-width; 5 | height: $device-height; 6 | background-color: $white; 7 | color: $black; 8 | padding: 42px; 9 | } 10 | 11 | .header { 12 | width: $device-width; 13 | margin-bottom: 42px; 14 | } 15 | 16 | $line-height: 12; 17 | 18 | .textAsList { 19 | white-space: pre-wrap; 20 | font-size: 12px; 21 | font-family: monospace; 22 | line-height: $line-height + px; 23 | } 24 | 25 | :export { 26 | line-height: $line-height; 27 | } 28 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconShuffle.tsx: -------------------------------------------------------------------------------- 1 | import { IconShuffle } from '@spotify-internal/encore-web'; 2 | import { IconSize } from '@spotify-internal/encore-web/types/src/core/components/Icon/Svg'; 3 | 4 | interface Props { 5 | className?: string; 6 | iconSize: number; 7 | } 8 | 9 | const CarThingIconShuffle = ({ className, iconSize }: Props) => ( 10 | 16 | ); 17 | 18 | export default CarThingIconShuffle; 19 | -------------------------------------------------------------------------------- /context/store.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, PropsWithChildren, useContext } from 'react'; 2 | import { RootStore, RootStoreProps } from 'store/RootStore'; 3 | 4 | export const StoreContext = createContext({} as RootStore); 5 | 6 | const StoreProvider = ({ 7 | store, 8 | children, 9 | }: PropsWithChildren) => { 10 | return ( 11 | {children} 12 | ); 13 | }; 14 | 15 | export const useStore = (): RootStore => useContext(StoreContext); 16 | 17 | export default StoreProvider; 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | /dist 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | .idea 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | .pnpm-debug.log* 29 | 30 | # local env files 31 | .env*.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | next-env.d.ts 39 | -------------------------------------------------------------------------------- /component/Npv/ControlButtons/Block.tsx: -------------------------------------------------------------------------------- 1 | import ControlButton from 'component/Npv/ControlButtons/ControlButton'; 2 | import { NpvIcon } from 'component/Npv/ControlButtons/Controls'; 3 | import { IconBlock } from 'component/CarthingUIComponents/'; 4 | import { useStore } from 'context/store'; 5 | 6 | const Block = () => { 7 | const uiState = useStore().npvStore.controlButtonsUiState; 8 | 9 | return ( 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default Block; 17 | -------------------------------------------------------------------------------- /@spotify-internal/ubi-logger-js/src/providers/PageInstanceIdProvider.ts: -------------------------------------------------------------------------------- 1 | export interface PageInstanceIdProvider { 2 | getPageInstanceId(): string | null; 3 | setPageInstanceId(pageInstanceId: string): void; 4 | } 5 | 6 | export class UBIPageInstanceIdProvider implements PageInstanceIdProvider { 7 | private _currentPageInstanceId: string | null = null; 8 | 9 | setPageInstanceId(pageInstanceId: string): void { 10 | this._currentPageInstanceId = pageInstanceId; 11 | } 12 | getPageInstanceId(): string | null { 13 | return this._currentPageInstanceId; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /component/Npv/Scrubbing/ScrubbingBar.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | $progress-bar-height: 4px; 4 | $container-height: -4px; 5 | 6 | .scrubbingBar { 7 | @include transition-background-color; 8 | 9 | position: absolute; 10 | z-index: $overlay-z-index + 1; 11 | width: $device-width; 12 | height: $progress-bar-height; 13 | background-image: linear-gradient(0deg, rgb(0 0 0 / 50%), rgb(0 0 0 / 50%)); 14 | } 15 | 16 | .progressPlayed { 17 | z-index: $scrubber-z-index + 1; 18 | background-color: $white; 19 | width: 2px; 20 | height: 100%; 21 | } 22 | -------------------------------------------------------------------------------- /component/Shelf/ShelfItem/InlineTipItem.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .inlineTipItem { 4 | width: 508px; 5 | height: 324px; 6 | display: flex; 7 | flex-direction: column; 8 | justify-content: space-between; 9 | @include transition-opacity-transform; 10 | } 11 | 12 | .subtitle { 13 | display: flex; 14 | } 15 | 16 | .tryThis { 17 | color: $green-light; 18 | } 19 | 20 | .toTheLeft { 21 | transform: translateX(-18px); 22 | } 23 | 24 | .hidden { 25 | opacity: 0; 26 | } 27 | 28 | .voiceTip { 29 | width: 448px; 30 | height: 304px; 31 | } 32 | -------------------------------------------------------------------------------- /@spotify-internal/ubi-logger-js/src/ubi.js: -------------------------------------------------------------------------------- 1 | import { UBILogger } from './loggers/UBILogger'; 2 | export const UBI = (function fn() { 3 | let ubiLogger; 4 | function initUBILogger(options) { 5 | if (ubiLogger) { 6 | ubiLogger.unregisterEventListeners(); 7 | } 8 | ubiLogger = new UBILogger(options); 9 | ubiLogger.registerEventListeners(); 10 | return ubiLogger; 11 | } 12 | return { 13 | getUBILogger: function getUBILoggerFn(options) { 14 | return initUBILogger(options); 15 | }, 16 | }; 17 | })(); 18 | -------------------------------------------------------------------------------- /component/Modals/LoginRequired.tsx: -------------------------------------------------------------------------------- 1 | import styles from './Modal.module.scss'; 2 | import { SpotifyLogo } from 'component/CarthingUIComponents'; 3 | 4 | const LoginRequired = () => { 5 | return ( 6 |
7 | 8 |
未登录
9 |
10 |

11 | 要使用 Car Thing ,您需要在您的手机上登陆 Spotify 应用程序。 12 |

13 |
14 |
15 | ); 16 | }; 17 | 18 | export default LoginRequired; 19 | -------------------------------------------------------------------------------- /component/Onboarding/SkipButton.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .skipButtonWrapper { 4 | color: $gray-70; 5 | border: 4px solid $gray-20; 6 | border-radius: 50%; 7 | width: 128px; 8 | height: 128px; 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | } 13 | 14 | .pressed { 15 | opacity: 0.5; 16 | } 17 | 18 | .animationEnter { 19 | opacity: 0; 20 | } 21 | 22 | .animationEnterActive { 23 | @include transition-opacity(800ms, ease-out, 1500ms); 24 | 25 | opacity: 1; 26 | } 27 | 28 | .animationEnterDone { 29 | opacity: 1; 30 | } 31 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/AppendEllipsis/AppendEllipsis.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import styles from './AppendEllipsis.module.scss'; 3 | 4 | type Props = { 5 | children: string; 6 | }; 7 | 8 | const Ellipsis = ({ children }: Props) => { 9 | return ( 10 | <> 11 | {children} 12 | . 13 | . 14 | . 15 | 16 | ); 17 | }; 18 | 19 | export default Ellipsis; 20 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconPlaybackSpeed1x.tsx: -------------------------------------------------------------------------------- 1 | import { IconPlaybackSpeed1x } from '@spotify-internal/encore-web'; 2 | import { IconSize } from '@spotify-internal/encore-web/types/src/core/components/Icon/Svg'; 3 | 4 | interface Props { 5 | className?: string; 6 | iconSize: number; 7 | } 8 | 9 | const CarThingIconPlaybackSpeed1x = ({ className, iconSize }: Props) => ( 10 | 16 | ); 17 | 18 | export default CarThingIconPlaybackSpeed1x; 19 | -------------------------------------------------------------------------------- /component/SwipeDownHandle/SwipeDownHandle.tsx: -------------------------------------------------------------------------------- 1 | import styles from './SwipeDownHandle.module.scss'; 2 | import { useSwipeable } from 'react-swipeable'; 3 | import { useStore } from 'context/store'; 4 | 5 | const SwipeDownHandle = () => { 6 | const uiState = useStore().swipeDownUiState; 7 | 8 | const swipeableProps = useSwipeable({ 9 | onSwipedDown: uiState.onSwipeDown, 10 | }); 11 | 12 | return ( 13 |
18 | ); 19 | }; 20 | 21 | export default SwipeDownHandle; 22 | -------------------------------------------------------------------------------- /component/Modals/NoNetwork.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | $container-width: 624px; 4 | 5 | .noNetworkWrapper { 6 | display: grid; 7 | height: $device-height; 8 | width: $device-width; 9 | background-color: $black; 10 | 11 | & > * { 12 | margin: auto; 13 | } 14 | } 15 | 16 | .noNetwork { 17 | display: grid; 18 | width: $container-width; 19 | gap: 24px; 20 | 21 | & > * { 22 | margin: auto; 23 | } 24 | } 25 | 26 | .noNetworkContent { 27 | display: grid; 28 | text-align: center; 29 | gap: 16px; 30 | 31 | & > * { 32 | margin: auto; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /component/CountUpTimer/CountUpTimer.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { secondsFormat, secondsToMinutesFormat } from 'helpers/TimeUtils'; 3 | 4 | const CountUpTimer = () => { 5 | const [seconds, setSeconds] = useState(0); 6 | useEffect(() => { 7 | const myInterval = setInterval(() => { 8 | setSeconds(seconds + 1); 9 | }, 1000); 10 | return () => { 11 | clearInterval(myInterval); 12 | }; 13 | }); 14 | 15 | return ( 16 | <> 17 | {secondsToMinutesFormat(seconds)}:{secondsFormat(seconds)} 18 | 19 | ); 20 | }; 21 | 22 | export default CountUpTimer; 23 | -------------------------------------------------------------------------------- /component/Npv/Tips/Tips.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .tips { 4 | height: 100%; 5 | width: 100%; 6 | box-sizing: border-box; 7 | padding: 32px 48px 32px 40px; 8 | } 9 | 10 | .title { 11 | color: $green-light; 12 | overflow: hidden; 13 | white-space: nowrap; 14 | text-overflow: ellipsis; 15 | } 16 | 17 | .topBar { 18 | display: flex; 19 | justify-content: space-between; 20 | margin-right: -8px; 21 | } 22 | 23 | .description { 24 | margin-top: 16px; 25 | display: -webkit-box; 26 | overflow: hidden; 27 | -webkit-line-clamp: 3; 28 | -webkit-box-orient: vertical; 29 | } 30 | -------------------------------------------------------------------------------- /@spotify-internal/ubi-logger-js/src/ubi.ts: -------------------------------------------------------------------------------- 1 | import { UBILogger, UBILoggerOptions } from './loggers/UBILogger'; 2 | 3 | export const UBI = (function fn() { 4 | let ubiLogger: UBILogger; 5 | 6 | function initUBILogger(options: UBILoggerOptions) { 7 | if (ubiLogger) { 8 | ubiLogger.unregisterEventListeners(); 9 | } 10 | 11 | ubiLogger = new UBILogger(options); 12 | ubiLogger.registerEventListeners(); 13 | return ubiLogger; 14 | } 15 | 16 | return { 17 | getUBILogger: function getUBILoggerFn(options: UBILoggerOptions) { 18 | return initUBILogger(options); 19 | }, 20 | }; 21 | })(); 22 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconPlaybackSpeed1point2x.tsx: -------------------------------------------------------------------------------- 1 | import { IconPlaybackSpeed1point2x } from '@spotify-internal/encore-web'; 2 | import { IconSize } from '@spotify-internal/encore-web/types/src/core/components/Icon/Svg'; 3 | interface Props { 4 | className?: string; 5 | iconSize: number; 6 | } 7 | 8 | const CarThingIconPlaybackSpeed1point2x = ({ className, iconSize }: Props) => ( 9 | 15 | ); 16 | 17 | export default CarThingIconPlaybackSpeed1point2x; 18 | -------------------------------------------------------------------------------- /component/Settings/Submenu/SubmenuHeader.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import styles from './SubmenuHeader.module.scss'; 3 | import Type from '../../CarthingUIComponents/Type/Type'; 4 | 5 | type Props = { 6 | icon: ReactNode; 7 | name: string; 8 | }; 9 | const SubmenuHeader = ({ icon, name }: Props) => { 10 | return ( 11 |
12 |
13 | {icon} 14 | 15 | {name} 16 | 17 |
18 |
19 | ); 20 | }; 21 | 22 | export default SubmenuHeader; 23 | -------------------------------------------------------------------------------- /component/Setup/Waiting.tsx: -------------------------------------------------------------------------------- 1 | import styles from './Waiting.module.scss'; 2 | import { SetupView } from 'store/SetupStore'; 3 | import AppendEllipsis from 'component/CarthingUIComponents/AppendEllipsis/AppendEllipsis'; 4 | 5 | const Waiting = () => { 6 | return ( 7 |
8 |
9 | 请稍等 10 |
11 |
12 | 在您的手机下载最新版本并重新连接到 Car Thing 时,安装将继续进行。 13 |
14 |
15 | ); 16 | }; 17 | 18 | export default Waiting; 19 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconPlaybackSpeed1point5x.tsx: -------------------------------------------------------------------------------- 1 | import { IconPlaybackSpeed1point5x } from '@spotify-internal/encore-web'; 2 | import { IconSize } from '@spotify-internal/encore-web/types/src/core/components/Icon/Svg'; 3 | 4 | interface Props { 5 | className?: string; 6 | iconSize: number; 7 | } 8 | 9 | const CarThingIconPlaybackSpeed1point5x = ({ className, iconSize }: Props) => ( 10 | 16 | ); 17 | 18 | export default CarThingIconPlaybackSpeed1point5x; 19 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconShuffleActive.tsx: -------------------------------------------------------------------------------- 1 | import IconShuffle from 'component/CarthingUIComponents/Icons/IconShuffle'; 2 | 3 | interface Props { 4 | className?: string; 5 | iconSize: number; 6 | strokeWidth?: number; 7 | } 8 | 9 | const IconShuffleActive = ({ className, iconSize }: Props) => { 10 | return ( 11 |
12 | 13 | 14 | 15 | 16 |
17 | ); 18 | }; 19 | 20 | export default IconShuffleActive; 21 | -------------------------------------------------------------------------------- /component/Presets/PresetCard/PresetContent.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | $preset-card-width: 168px; 4 | 5 | .presetTitle { 6 | margin-top: 24px; 7 | display: flex; 8 | flex-direction: column; 9 | align-items: center; 10 | white-space: nowrap; 11 | 12 | div { 13 | overflow: hidden; 14 | text-overflow: ellipsis; 15 | padding: 0; 16 | text-align: center; 17 | } 18 | 19 | .title { 20 | width: $preset-card-width; 21 | color: $opacity-white-70; 22 | } 23 | 24 | .active { 25 | color: $white; 26 | } 27 | 28 | .subtitle { 29 | width: $preset-card-width; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /component/NightMode/NightModeUbiLogger.ts: -------------------------------------------------------------------------------- 1 | import UbiLogger from 'eventhandler/UbiLogger'; 2 | import { createCarNightModeCarthingosEventFactory } from '@spotify-internal/ubi-sdk-music-car-night-mode-carthingos'; 3 | 4 | class NightModeUbiLogger { 5 | ubiLogger: UbiLogger; 6 | 7 | constructor(ubiLogger: UbiLogger) { 8 | this.ubiLogger = ubiLogger; 9 | } 10 | 11 | logNightModeImpression = (reason: string) => { 12 | const event = createCarNightModeCarthingosEventFactory({ 13 | data: { reason: reason }, 14 | }).impression(); 15 | this.ubiLogger.logImpression(event); 16 | }; 17 | } 18 | 19 | export default NightModeUbiLogger; 20 | -------------------------------------------------------------------------------- /component/Npv/Npv.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .npv { 4 | position: relative; 5 | width: $device-width; 6 | height: $device-height; 7 | overflow: hidden; 8 | transition-delay: $duration-default; 9 | } 10 | 11 | .controlsContainer { 12 | height: 144px; 13 | position: absolute; 14 | width: 100%; 15 | bottom: 0; 16 | background: $opacity-black-30; 17 | } 18 | 19 | .enter { 20 | opacity: 0; 21 | } 22 | 23 | .enterActive { 24 | opacity: 1; 25 | 26 | @include transition-opacity; 27 | } 28 | 29 | .exit { 30 | opacity: 1; 31 | } 32 | 33 | .exitActive { 34 | opacity: 0; 35 | 36 | @include transition-opacity; 37 | } 38 | -------------------------------------------------------------------------------- /component/Npv/PlayingInfo/PlayingInfoHeader.tsx: -------------------------------------------------------------------------------- 1 | import { useStore } from 'context/store'; 2 | import { observer } from 'mobx-react-lite'; 3 | import styles from './PlayingInfoHeader.module.scss'; 4 | 5 | const PlayingInfoHeader = () => { 6 | const uiState = useStore().npvStore.playingInfoUiState; 7 | 8 | return ( 9 |
10 |
15 | {uiState.contextHeaderTitle} 16 |
17 |
18 | ); 19 | }; 20 | 21 | export default observer(PlayingInfoHeader); 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": false, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "baseUrl": "./", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react-jsx" 19 | }, 20 | "include": ["."], 21 | "references": [{ "path": "./tsconfig.node.json" }] 22 | } 23 | -------------------------------------------------------------------------------- /component/Tracklist/ActionConfirmation/QueueConfirmationBanner.tsx: -------------------------------------------------------------------------------- 1 | import { observer } from 'mobx-react-lite'; 2 | import { useStore } from 'context/store'; 3 | import Banner from 'component/CarthingUIComponents/Banner/Banner'; 4 | import { IconCheck32 } from 'component/CarthingUIComponents'; 5 | 6 | const QueueConfirmationBanner = () => { 7 | const uiState = useStore().tracklistStore.tracklistUiState; 8 | 9 | return ( 10 | } 13 | infoText="已添加到队列。" 14 | colorStyle="confirmation" 15 | /> 16 | ); 17 | }; 18 | 19 | export default observer(QueueConfirmationBanner); 20 | -------------------------------------------------------------------------------- /helpers/TextUtil.ts: -------------------------------------------------------------------------------- 1 | export const firstLetterUpperCase = function firstLetterUpperCase( 2 | text: string, 3 | ): string { 4 | if (!text) return text; 5 | return text.substr(0, 1).toUpperCase() + text.substr(1); 6 | }; 7 | 8 | export const parsePinCodeSpacing = function parsePinCodeSpacing( 9 | text: string | undefined, 10 | ) { 11 | if (!text) return text; 12 | return text.substr(0, 3).concat(' ') + text.substr(3).trim(); 13 | }; 14 | 15 | export const removeAndCapitaliseAfterX = (text: string, cutAt: number) => { 16 | if (!text) return text; 17 | const newString = text.substr(cutAt); 18 | return newString.substr(0, 1) + newString.substr(1).toLowerCase(); 19 | }; 20 | -------------------------------------------------------------------------------- /public/images/menu-dots.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /eventhandler/index.ts: -------------------------------------------------------------------------------- 1 | import HardwareEvents from 'helpers/HardwareEvents'; 2 | import { RootStore } from 'store/RootStore'; 3 | import reactToBackButton from './BackButtonHandler'; 4 | import reactToDial from './DialHandler'; 5 | import reactToPresetButtons from './PresetButtonsHandler'; 6 | import reactToSettingsButton from './SettingsButtonHandler'; 7 | 8 | export default { 9 | handleEvents: (hardwareEvents: HardwareEvents, rootStore: RootStore) => { 10 | reactToBackButton(hardwareEvents, rootStore); 11 | reactToDial(hardwareEvents, rootStore); 12 | reactToPresetButtons(hardwareEvents, rootStore); 13 | reactToSettingsButton(hardwareEvents, rootStore); 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /App.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | :root { 4 | --font-family: spotify-circular; 5 | } 6 | 7 | body { 8 | user-select: none; 9 | box-sizing: border-box; 10 | color: white; 11 | } 12 | 13 | #corners { 14 | position: absolute; 15 | pointer-events: none; 16 | z-index: $corners-z-index; 17 | } 18 | 19 | #container { 20 | position: relative; 21 | width: $device-width; 22 | height: $device-height; 23 | max-width: $device-width; 24 | max-height: $device-height; 25 | margin: 0 auto; 26 | overflow: hidden; 27 | @include transition-opacity(1000ms); 28 | } 29 | 30 | *:focus { 31 | outline: none; 32 | } 33 | 34 | p, 35 | h1, 36 | h2 { 37 | margin: 0; 38 | } 39 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/ButtonGroup/ButtonGroup.tsx: -------------------------------------------------------------------------------- 1 | import styles from './ButtonGroup.module.scss'; 2 | import { CSSProperties } from 'react'; 3 | import classnames from 'classnames'; 4 | 5 | type Layout = 'horizontal' | 'vertical'; 6 | 7 | export interface Props { 8 | children: React.ReactNode; 9 | layout?: Layout; 10 | style?: CSSProperties; 11 | } 12 | 13 | export default function ButtonGroup({ children, style, layout }: Props) { 14 | return ( 15 |
21 | {children} 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /component/Npv/OtherMedia/OtherMediaUiState.ts: -------------------------------------------------------------------------------- 1 | import PlayerStore from 'store/PlayerStore'; 2 | import SessionStateStore from 'store/SessionStateStore'; 3 | 4 | export const createOtherMediaUiState = ( 5 | playerStore: PlayerStore, 6 | sessionStateStore: SessionStateStore, 7 | ) => ({ 8 | get shouldShowContent() { 9 | return sessionStateStore.isLoggedIn; 10 | }, 11 | get currentItem() { 12 | return playerStore.currentTrack; 13 | }, 14 | get otherActiveApp() { 15 | return playerStore.otherActiveApp; 16 | }, 17 | get title() { 18 | return playerStore.currentTrack.name; 19 | }, 20 | get subtitle() { 21 | return this.currentItem.artist.name; 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /@spotify-internal/uri/src/enums/prefix.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The URI prefix for URIs. 3 | */ 4 | export const URI_PREFIX = 'spotify:'; 5 | /** 6 | * The URL prefix for Play. 7 | */ 8 | export const PLAY_HTTP_PREFIX = 'http://play.spotify.com/'; 9 | /** 10 | * The HTTPS URL prefix for Play. 11 | */ 12 | export const PLAY_HTTPS_PREFIX = 'https://play.spotify.com/'; 13 | /** 14 | * The URL prefix for Open. 15 | */ 16 | export const OPEN_HTTP_PREFIX = 'http://open.spotify.com/'; 17 | /** 18 | * The HTTPS URL prefix for Open. 19 | */ 20 | export const OPEN_HTTPS_PREFIX = 'https://open.spotify.com/'; 21 | /** 22 | * The path prefix for paths without domain prefix. 23 | */ 24 | export const PATH_PREFIX = '/'; 25 | -------------------------------------------------------------------------------- /component/Modals/NonSupportedType.tsx: -------------------------------------------------------------------------------- 1 | import { IconExclamationAlt } from '@spotify-internal/encore-web'; 2 | import LegacyModal from 'component/Modals/LegacyModal'; 3 | import styles from './NonSupportedType.module.scss'; 4 | 5 | const NonSupportedType = () => { 6 | return ( 7 | 8 |
12 | 13 |
14 | 歌曲界面不适用于专辑电台 15 |
16 |
17 |
18 | ); 19 | }; 20 | 21 | export default NonSupportedType; 22 | -------------------------------------------------------------------------------- /component/Setup/BTPairing.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .screen { 4 | background: $aubergine; 5 | height: $device-height; 6 | width: $device-width; 7 | padding: 32px 48px 40px; 8 | } 9 | 10 | .content { 11 | height: 260px; 12 | display: flex; 13 | justify-content: space-between; 14 | flex-direction: column; 15 | } 16 | 17 | .title { 18 | margin-bottom: 32px; 19 | color: $pink; 20 | 21 | @include text-style(120px, 120px, -0.04em); 22 | } 23 | 24 | .texts { 25 | color: $pink; 26 | width: 650px; 27 | 28 | @include text-style(36px, 48px, -0.02em); 29 | } 30 | 31 | .pinCode { 32 | color: $pink; 33 | 34 | @include text-style(72px, 72px, -0.03em); 35 | } 36 | -------------------------------------------------------------------------------- /@spotify-internal/encore-web/es/components/FormToggle/Label.js: -------------------------------------------------------------------------------- 1 | import { spacer12, spacer24 } from '@spotify-internal/encore-foundation'; 2 | import styled from 'styled-components'; 3 | import { cssColorValue, cursorDisabled, finale, opacityDisabled, semanticColors, viola } from "../../styles"; 4 | export var Label = styled.span.withConfig({ 5 | displayName: "Label", 6 | componentId: "sc-1sbwovc-0" 7 | })(["color:", ";", ";padding-inline-start:", ";padding-inline-end:", ";min-inline-size:0;overflow-wrap:break-word;input:disabled ~ &{cursor:", ";opacity:", ";}"], cssColorValue(semanticColors.textBase), function (props) { 8 | return props.small ? finale() : viola(); 9 | }, spacer12, spacer24, cursorDisabled, opacityDisabled); -------------------------------------------------------------------------------- /component/Npv/Scrubbing/ScrubbingBackdrop.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .scrubbingBackdrop { 4 | position: absolute; 5 | width: $device-width; 6 | height: $device-height; 7 | background: rgb(0 0 0 / 85%); 8 | top: 0; 9 | left: 0; 10 | display: flex; 11 | flex-direction: column; 12 | align-items: center; 13 | z-index: $scrubbing-overlay-z-index; 14 | } 15 | 16 | .time { 17 | width: $device-width - 80px; 18 | height: $device-height - 180px; 19 | display: flex; 20 | justify-content: space-between; 21 | 22 | span { 23 | @include text-style(72px, 72px, -0.03em); 24 | 25 | height: 72px; 26 | color: $white; 27 | align-self: flex-end; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /@spotify-internal/encore-web/es/components/ButtonPrimary/ButtonFocus.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { buttonBorderRadius, focusGapBorderStyle } from "../../styles"; 3 | 4 | /* ButtonFocus contains ButtonPrimary's focus state, which is themed using the color set 5 | * of ButtonPrimary's _parent_, and not the colorSet of ButtonPrimary itself. 6 | * ButtonFocus is always in the DOM, but is invisible unless the button focused & isUsingKeyboard is true 7 | */ 8 | export var ButtonFocus = styled.span.withConfig({ 9 | displayName: "ButtonFocus", 10 | componentId: "sc-2hq6ey-0" 11 | })(["", ""], function (props) { 12 | return props.isUsingKeyboard && focusGapBorderStyle(buttonBorderRadius); 13 | }); -------------------------------------------------------------------------------- /component/Shelf/ShelfHeader/ShelfHeaderItem.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .withTransition { 4 | @include transition-transform; 5 | } 6 | 7 | .titleContainer { 8 | display: flex; 9 | opacity: 0.5; 10 | padding: 28px 0; 11 | margin: -28px 0; 12 | 13 | &.active { 14 | opacity: 1; 15 | } 16 | 17 | &.hidden { 18 | opacity: 0; 19 | } 20 | } 21 | 22 | .titleIcon { 23 | display: flex; 24 | align-items: center; 25 | } 26 | 27 | .titleText { 28 | @include text-style(32px, 40px, -0.02em); 29 | 30 | font-weight: bold; 31 | color: $white; 32 | white-space: nowrap; 33 | 34 | @include transition-opacity; 35 | 36 | &.hidden { 37 | opacity: 0; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /component/Npv/Scrubbing/Scrubbing.tsx: -------------------------------------------------------------------------------- 1 | import { useStore } from 'context/store'; 2 | import styles from './Scrubbing.module.scss'; 3 | import { observer } from 'mobx-react-lite'; 4 | import ScrubbingBar from 'component/Npv/Scrubbing/ScrubbingBar'; 5 | 6 | const Scrubbing = () => { 7 | const uiState = useStore().npvStore.scrubbingUiState; 8 | 9 | return ( 10 | <> 11 | {uiState.isScrubbingEnabled && ( 12 |
17 | )} 18 | 19 | 20 | ); 21 | }; 22 | 23 | export default observer(Scrubbing); 24 | -------------------------------------------------------------------------------- /component/Npv/Volume/Volume.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .volume { 4 | height: 144px; 5 | width: 100%; 6 | position: absolute; 7 | bottom: 0; 8 | display: flex; 9 | flex-direction: column; 10 | justify-content: center; 11 | align-items: center; 12 | 13 | svg { 14 | color: $white; 15 | } 16 | 17 | .volumeInfo { 18 | margin-top: 24px; 19 | width: 342px; 20 | display: flex; 21 | justify-content: center; 22 | align-items: center; 23 | 24 | .volumeIcon { 25 | width: 40px; 26 | height: 40px; 27 | display: flex; 28 | justify-content: center; 29 | align-items: center; 30 | margin-right: 15px; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /component/Setup/Updating.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .screen { 4 | background: $aubergine; 5 | height: $device-height; 6 | width: $device-width; 7 | padding: 32px 48px 40px; 8 | } 9 | 10 | .content { 11 | height: 260px; 12 | display: flex; 13 | justify-content: space-between; 14 | flex-direction: column; 15 | } 16 | 17 | .title { 18 | color: $pink; 19 | margin-bottom: 32px; 20 | 21 | @include text-style(120px, 120px, -0.04em); 22 | } 23 | 24 | .subtitle { 25 | color: $pink; 26 | 27 | @include text-style(36px, 48px, -0.02em); 28 | 29 | max-width: 700px; 30 | } 31 | 32 | .progress { 33 | color: $pink; 34 | 35 | @include text-style(72px, 72px, -0.03em); 36 | } 37 | -------------------------------------------------------------------------------- /eventhandler/HardwareEventHandler.ts: -------------------------------------------------------------------------------- 1 | import HardwareEvents from 'helpers/HardwareEvents'; 2 | import { RootStore } from 'store/RootStore'; 3 | import reactToBackButton from './BackButtonHandler'; 4 | import reactToDial from './DialHandler'; 5 | import reactToPresetButtons from './PresetButtonsHandler'; 6 | import reactToSettingsButton from './SettingsButtonHandler'; 7 | 8 | export default { 9 | handleEvents: (hardwareEvents: HardwareEvents, rootStore: RootStore) => { 10 | reactToBackButton(hardwareEvents, rootStore); 11 | reactToDial(hardwareEvents, rootStore); 12 | reactToPresetButtons(hardwareEvents, rootStore); 13 | reactToSettingsButton(hardwareEvents, rootStore); 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /@spotify-internal/uri/src/enums/prefix.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The URI prefix for URIs. 3 | */ 4 | export const URI_PREFIX = 'spotify:'; 5 | 6 | /** 7 | * The URL prefix for Play. 8 | */ 9 | export const PLAY_HTTP_PREFIX = 'http://play.spotify.com/'; 10 | 11 | /** 12 | * The HTTPS URL prefix for Play. 13 | */ 14 | export const PLAY_HTTPS_PREFIX = 'https://play.spotify.com/'; 15 | 16 | /** 17 | * The URL prefix for Open. 18 | */ 19 | export const OPEN_HTTP_PREFIX = 'http://open.spotify.com/'; 20 | 21 | /** 22 | * The HTTPS URL prefix for Open. 23 | */ 24 | export const OPEN_HTTPS_PREFIX = 'https://open.spotify.com/'; 25 | 26 | /** 27 | * The path prefix for paths without domain prefix. 28 | */ 29 | export const PATH_PREFIX = '/'; 30 | -------------------------------------------------------------------------------- /component/Presets/PresetCard/PresetPlaceholder.tsx: -------------------------------------------------------------------------------- 1 | import { observer } from 'mobx-react-lite'; 2 | import styles from 'component/Presets/PresetCard/PresetPlaceholder.module.scss'; 3 | import Type from 'component/CarthingUIComponents/Type/Type'; 4 | import classNames from 'classnames'; 5 | 6 | type Props = { 7 | isFocused: boolean; 8 | }; 9 | const PresetPlaceholder = ({ isFocused }: Props) => { 10 | return ( 11 |
12 | 16 | 按住预设按钮来保存正在播放的内容 17 | 18 |
19 | ); 20 | }; 21 | 22 | export default observer(PresetPlaceholder); 23 | -------------------------------------------------------------------------------- /component/Presets/SavingPresetFailed.tsx: -------------------------------------------------------------------------------- 1 | import styles from 'component/Modals/Modal.module.scss'; 2 | import savingPresetStyles from './SavingPresetFailed.module.scss'; 3 | 4 | import { IconExclamationAlt } from '@spotify-internal/encore-web'; 5 | import Type from 'component/CarthingUIComponents/Type/Type'; 6 | 7 | const SavingPresetFailed = () => { 8 | return ( 9 |
10 | 11 |
12 | 13 | 当前无法保存到预设。
14 | 请稍后再试。 15 |
16 |
17 |
18 | ); 19 | }; 20 | 21 | export default SavingPresetFailed; 22 | -------------------------------------------------------------------------------- /public/images/round-corners.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 10 | 14 | 18 | -------------------------------------------------------------------------------- /helpers/Pagination.ts: -------------------------------------------------------------------------------- 1 | export const shouldFetchMore = ( 2 | minDistanceFromEnd: number, 3 | currentLength: number, 4 | currentIndex: number, 5 | totalAvailable: number, 6 | ): boolean => { 7 | return ( 8 | currentLength - currentIndex < minDistanceFromEnd && 9 | currentLength < totalAvailable 10 | ); 11 | }; 12 | 13 | export const getNextOffset = ( 14 | currentOffset: number, 15 | pageSize: number, 16 | totalAvailable: number, 17 | ): number => { 18 | return Math.min(currentOffset + pageSize, totalAvailable); 19 | }; 20 | 21 | export const getNextLimit = (pageSize, currentNumberOfItems, totalAvailable) => 22 | pageSize + currentNumberOfItems >= totalAvailable 23 | ? totalAvailable - currentNumberOfItems 24 | : pageSize; 25 | -------------------------------------------------------------------------------- /component/Setup/SetupHelp.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .screen { 4 | background: $aubergine; 5 | height: $device-height; 6 | width: $device-width; 7 | padding: 32px 48px 40px; 8 | } 9 | 10 | .subtitle { 11 | color: $pink; 12 | 13 | @include text-style(36px, 48px, -0.03em); 14 | 15 | max-width: 704px; 16 | } 17 | 18 | .texts { 19 | height: 300px; 20 | } 21 | 22 | .content { 23 | width: 704px; 24 | height: 410px; 25 | display: flex; 26 | flex-direction: column; 27 | justify-content: space-between; 28 | } 29 | 30 | .backToSetup { 31 | color: $pink; 32 | margin-top: 20px; 33 | 34 | @include text-style(36px, 40px, -0.02em); 35 | 36 | font-weight: bold; 37 | } 38 | 39 | .white { 40 | color: $white; 41 | } 42 | -------------------------------------------------------------------------------- /component/OtaUpdating/OtaUpdating.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .background { 4 | width: 100%; 5 | height: 100%; 6 | padding: 32px 72px 36px 40px; 7 | position: relative; 8 | background-color: $black; 9 | } 10 | 11 | .error { 12 | @include text-style(120px, 120px, -0.04em); 13 | 14 | color: $white; 15 | } 16 | 17 | .title { 18 | @include text-style(120px, 120px, -0.04em); 19 | 20 | color: $white; 21 | } 22 | 23 | .subtitle { 24 | height: 120px; 25 | margin-top: 32px; 26 | 27 | @include text-style(36px, 48px, -0.02em); 28 | 29 | color: $white; 30 | } 31 | 32 | .progress { 33 | @include text-style(68px, 72px, -0.04em); 34 | 35 | color: $green-light; 36 | position: absolute; 37 | left: 36px; 38 | bottom: 48px; 39 | } 40 | -------------------------------------------------------------------------------- /helpers/contentIdExtractor.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Example input and output see tests fore more: 3 | spotify:space_item:superbird:superbird-featured -> featured 4 | spotify:spaces:superbird?blockIdentifier=superbird-featured -> featured 5 | */ 6 | 7 | export const parseCategoryId = function parseCategoryId(text: string) { 8 | const parsedText = text.endsWith('wrapper') 9 | ? text.split('-wrapper')[0] 10 | : text; 11 | const lastEqualChar = parsedText.lastIndexOf('='); 12 | const lastColonChar = parsedText.lastIndexOf(':'); 13 | const lastDashChar = parsedText.lastIndexOf('-'); 14 | 15 | const lastSplitChar = Math.max(lastEqualChar, lastColonChar, lastDashChar); 16 | return lastSplitChar !== -1 17 | ? parsedText.substring(lastSplitChar + 1) 18 | : parsedText; 19 | }; 20 | -------------------------------------------------------------------------------- /component/Views/Views.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .viewArea { 4 | height: $device-height; 5 | width: $device-width; 6 | position: relative; 7 | overflow: hidden; 8 | } 9 | 10 | .view { 11 | position: absolute; 12 | top: 0; 13 | transform: perspective(800px) translate3d(0, 530px, -160px); 14 | width: 800px; 15 | height: 480px; 16 | filter: brightness(0.9); 17 | 18 | @include transition-filter-transform; 19 | @include rounded-corners; 20 | } 21 | 22 | .underCurrent { 23 | transform: perspective(800px) translate3d(0, 10px, -40px); 24 | filter: brightness(0.2); 25 | } 26 | 27 | .current { 28 | transition-delay: 100ms; 29 | transform: translateY(0); 30 | filter: brightness(1); 31 | } 32 | 33 | .forceOnTop { 34 | z-index: 1; 35 | } 36 | -------------------------------------------------------------------------------- /@spotify-internal/encore-web/es/components/GlobalStyles/index.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; // 2 | // Global 3 | // -------------------------------------------------- 4 | 5 | import Global from "../../styles/global-styles"; 6 | export var GlobalStyles = function GlobalStyles() { 7 | return /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement(Global, null)); 8 | }; 9 | /** 10 | * **GlobalStyles** ![Status: Production](https://img.shields.io/badge/PRODUCTION-%2357B560|height=14) 11 | * 12 | * [GitHub](https://ghe.spotify.net/encore/web/tree/master/src/core/components/GlobalStyles) | [Storybook](https://encore-web.spotify.net/?path=/docs/components-globalstyles--default) | 13 | * 14 | * null 15 | * 16 | * @example 17 | * () => ; 18 | * 19 | */ -------------------------------------------------------------------------------- /component/CarthingUIComponents/Button/Button.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | @mixin ct-button-base-style() { 4 | width: fit-content; 5 | padding: 24px 64px; 6 | font-family: inherit; 7 | font-weight: bold; 8 | border: none; 9 | border-radius: 44px; 10 | display: flex; 11 | align-items: center; 12 | justify-content: center; 13 | @include text-style(32px, 40px, -0.02em); 14 | 15 | &.pressed { 16 | opacity: 0.5; 17 | } 18 | } 19 | 20 | .buttonPrimary { 21 | @include ct-button-base-style; 22 | 23 | background-color: $white; 24 | color: $black; 25 | } 26 | 27 | .buttonSecondary { 28 | @include ct-button-base-style; 29 | 30 | background-color: transparent; 31 | color: $white; 32 | border: 4px solid $opacity-white-50; 33 | padding: 20px 60px; 34 | } 35 | -------------------------------------------------------------------------------- /component/Setup/Connected.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | import styles from './Connected.module.scss'; 3 | import { pink, aubergine } from '@spotify-internal/encore-foundation'; 4 | import { SetupView } from 'store/SetupStore'; 5 | 6 | class Connected extends Component<{}, {}> { 7 | render() { 8 | return ( 9 |
14 |
15 | 已连接 16 |
17 |
18 | 配置过程将在 Spotify 应用程序内继续进行。 19 |
20 |
21 | ); 22 | } 23 | } 24 | 25 | export default Connected; 26 | -------------------------------------------------------------------------------- /@spotify-internal/ubi-types-js/src/ubiTypes.js: -------------------------------------------------------------------------------- 1 | export var NavigationReason; 2 | (function (NavigationReason) { 3 | NavigationReason["CLIENT_LOST_FOCUS"] = "client_lost_focus"; 4 | NavigationReason["CLIENT_GAINED_FOCUS"] = "client_gained_focus"; 5 | NavigationReason["CLIENT_STARTED"] = "client_started"; 6 | NavigationReason["DEEP_LINK"] = "deep_link"; 7 | NavigationReason["BACK"] = "back"; 8 | NavigationReason["FORWARD"] = "forward"; 9 | NavigationReason["UNKNOWN"] = "unknown"; 10 | })(NavigationReason || (NavigationReason = {})); 11 | export function isNavigationByInteraction(navigationStartInfo) { 12 | return 'interactionId' in navigationStartInfo; 13 | } 14 | export function isNavigationByReason(navigationStartInfo) { 15 | return 'navigationReason' in navigationStartInfo; 16 | } 17 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/SpotifyLogo/SpotifyLogo.tsx: -------------------------------------------------------------------------------- 1 | import { LogoSpotify } from '@spotify-internal/encore-web'; 2 | import './SpotifyLogo.scss'; 3 | 4 | export type Props = { 5 | logoColorClass?: string; 6 | logoHeight?: number; 7 | condensed?: boolean; 8 | useBrandColor?: boolean; 9 | viewBox?: string; 10 | }; 11 | 12 | const SpotifyLogo = ({ 13 | logoColorClass, 14 | logoHeight, 15 | condensed = true, 16 | useBrandColor = false, 17 | viewBox = '0 0 26 24', 18 | }: Props) => { 19 | return ( 20 | 28 | ); 29 | }; 30 | 31 | export default SpotifyLogo; 32 | -------------------------------------------------------------------------------- /component/Queue/QueueHeader/QueueHeader.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .headerWrapper { 4 | height: 152px; 5 | display: flex; 6 | padding-left: 40px; 7 | padding-right: 104px; 8 | @include transition-background; 9 | } 10 | 11 | .header { 12 | width: 100%; 13 | display: flex; 14 | align-items: center; 15 | justify-content: space-between; 16 | 17 | @include transition-transform-width; 18 | 19 | &.smallHeader { 20 | transform: translateY(38px); 21 | width: 134%; 22 | } 23 | } 24 | 25 | .queueTitle { 26 | @include transition-transform; 27 | 28 | transform-origin: top left; 29 | overflow: hidden; 30 | text-overflow: ellipsis; 31 | white-space: nowrap; 32 | font-weight: bold; 33 | 34 | &.smallHeader { 35 | transform: scale(calc(32 / 44)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /component/Npv/OtherMedia/OtherMedia.tsx: -------------------------------------------------------------------------------- 1 | import BackToSpotify from 'component/Npv/OtherMedia/BackToSpotify/BackToSpotify'; 2 | import Widget from 'component/Npv/OtherMedia/Widget/Widget'; 3 | import StatusIcons from 'component/Npv/PlayingInfo/StatusIcons'; 4 | import { useStore } from 'context/store'; 5 | import { useEffect } from 'react'; 6 | import styles from './OtherMedia.module.scss'; 7 | 8 | const OtherMedia = () => { 9 | const controller = useStore().npvStore.otherMediaController; 10 | 11 | useEffect(() => controller.logImpression(), [controller]); 12 | 13 | return ( 14 |
15 |
16 | 17 | 18 |
19 | 20 |
21 | ); 22 | }; 23 | 24 | export default OtherMedia; 25 | -------------------------------------------------------------------------------- /component/Npv/Volume/VolumeBar.tsx: -------------------------------------------------------------------------------- 1 | import { useStore } from 'context/store'; 2 | import { observer } from 'mobx-react-lite'; 3 | import styles from './VolumeBar.module.scss'; 4 | 5 | const VolumeBar = () => { 6 | const uiState = useStore().npvStore.volumeUiState; 7 | const { colorChannels, isPlayingSpotify } = uiState; 8 | 9 | return ( 10 |
20 | 26 |
27 | ); 28 | }; 29 | 30 | export default observer(VolumeBar); 31 | -------------------------------------------------------------------------------- /component/Presets/PresetIndicator/PresetNumberIndicator.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .presetTopWrapper { 4 | width: 200px; 5 | margin: 0 auto; 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | z-index: $overlay-z-index + 1; 10 | } 11 | 12 | .presetIndicator { 13 | width: 80px; 14 | height: 4px; 15 | background: $opacity-white-30; 16 | clip-path: inset(0 0 0 0 round 0 0 4px 4px); 17 | } 18 | 19 | .presetNumber { 20 | margin: 16px 0 32px; 21 | 22 | .number { 23 | color: $opacity-white-70; 24 | } 25 | } 26 | 27 | .active { 28 | z-index: $overlay-z-index + 1; 29 | 30 | .presetNumber .number { 31 | color: $white; 32 | } 33 | 34 | .presetIndicator { 35 | background-color: $green-light; 36 | @include transition-background-color; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /component/Settings/PowerTutorial/PowerTutorial.tsx: -------------------------------------------------------------------------------- 1 | import { useStore } from 'context/store'; 2 | import { useEffect } from 'react'; 3 | import styles from './PowerTutorial.module.scss'; 4 | import Type from '../../CarthingUIComponents/Type/Type'; 5 | 6 | const PowerTutorial = () => { 7 | const { 8 | ubiLogger: { settingsUbiLogger }, 9 | } = useStore(); 10 | 11 | useEffect(() => { 12 | settingsUbiLogger.logPowerOffTutorialImpression(); 13 | }, [settingsUbiLogger]); 14 | 15 | return ( 16 |
17 | 18 | 打开/关闭电源 19 | 20 | 21 | 若要进行打开/关闭电源操作,请按住设备顶部的“设置”按钮。 22 | 23 |
24 | ); 25 | }; 26 | 27 | export default PowerTutorial; 28 | -------------------------------------------------------------------------------- /@spotify-internal/encore-web/es/styles/mixins/localization.js: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components'; 2 | /* Apply this mixin to elements with `display: flex` or `display: inline-flex` applied to properly 3 | support the break-word behavior (i.e. it will break long words only when absolutely necessary). 4 | We can transition to just applying `break-word: anywhere` once it is supported by Safari, but 5 | until then, this mixin allows us to fall back to the deprecated-but-supported `word-break` 6 | solution instead. 7 | 8 | For non-flex-container elements, apply `overflow-wrap: break-word'` directly instead. 9 | */ 10 | 11 | export var overflowWrapFlexText = function overflowWrapFlexText() { 12 | return css(["@supports (overflow-wrap:anywhere){overflow-wrap:anywhere;}@supports not (overflow-wrap:anywhere){word-break:break-word;}"]); 13 | }; -------------------------------------------------------------------------------- /component/Onboarding/BackPressBanner.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .banner { 4 | z-index: $onboarding-tactile-z-index; 5 | position: absolute; 6 | width: 351px; 7 | height: 96px; 8 | right: -45px; 9 | bottom: 24px; 10 | background: $white; 11 | border-radius: 48px; 12 | display: flex; 13 | align-items: center; 14 | padding: 0 40px; 15 | 16 | span { 17 | color: $black; 18 | font-weight: 700; 19 | 20 | @include text-style(36px, 40px, -0.02em); 21 | } 22 | 23 | svg { 24 | color: $green; 25 | margin-left: 30px; 26 | animation: ani 1.7s infinite; 27 | } 28 | } 29 | 30 | @keyframes ani { 31 | 0% { 32 | transform: scale(2.6); 33 | } 34 | 35 | 50% { 36 | transform: scale(2.6) translateX(8px); 37 | } 38 | 39 | 100% { 40 | transform: scale(2.6); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/NowPlaying/NowPlaying.tsx: -------------------------------------------------------------------------------- 1 | import styles from 'component/CarthingUIComponents/NowPlaying/NowPlaying.module.scss'; 2 | import Equaliser from 'component/CarthingUIComponents/Equaliser/Equaliser'; 3 | import Type, { TypeName } from 'component/CarthingUIComponents/Type/Type'; 4 | import { greenLight } from 'style/Variables'; 5 | 6 | export type Props = { 7 | playing?: boolean; 8 | textName: TypeName; 9 | }; 10 | const NowPlaying = ({ playing = false, textName }: Props) => { 11 | return ( 12 |
13 | 14 | 19 | 当前播放 20 | 21 |
22 | ); 23 | }; 24 | 25 | export default NowPlaying; 26 | -------------------------------------------------------------------------------- /component/Npv/PodcastSpeedOptions/PodcastSpeedOptions.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .podcastSpeedOverlay { 4 | z-index: $overlay-z-index + 1; 5 | background-color: $black; 6 | } 7 | 8 | .podcastSpeedOptions { 9 | width: $device-width; 10 | height: $device-height; 11 | border-radius: 0; 12 | position: relative; 13 | top: 0; 14 | left: 0; 15 | background: $black; 16 | 17 | @include transition-transform; 18 | 19 | &.smallHeader { 20 | transform: translateY(-48px); 21 | } 22 | } 23 | 24 | .header { 25 | height: 152px; 26 | width: 100%; 27 | display: flex; 28 | align-items: center; 29 | padding: 0 40px; 30 | } 31 | 32 | .title { 33 | @include transition-transform; 34 | 35 | transform-origin: left; 36 | 37 | &.smallTitle { 38 | transform: translateY(24px) scale(calc(32 / 44)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /hocs/withStore.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentType } from 'react'; 2 | import { RootStoreProps } from 'store/RootStore'; 3 | import { useStore } from 'context/store'; 4 | import hoistNonReactStatics from 'hoist-non-react-statics'; 5 | 6 | function getDisplayName(WrappedComponent) { 7 | return WrappedComponent.displayName || WrappedComponent.name || 'Component'; 8 | } 9 | 10 | const withStore =

(WrappedComponent: ComponentType

) => { 11 | const WithStore = (props: Omit) => { 12 | const store = useStore(); 13 | //@ts-ignore 14 | return ; 15 | }; 16 | WithStore.displayName = `WithStore(${getDisplayName(WrappedComponent)})`; 17 | 18 | hoistNonReactStatics(WithStore, WrappedComponent); 19 | 20 | return WithStore; 21 | }; 22 | 23 | export default withStore; 24 | -------------------------------------------------------------------------------- /component/Setup/Failed.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import styles from './Failed.module.scss'; 3 | import { useStore } from 'context/store'; 4 | import { SetupView } from 'store/SetupStore'; 5 | 6 | const Failed = () => { 7 | const { hardwareStore } = useStore(); 8 | const [rebootSelected, setRebootSelected] = useState(false); 9 | return ( 10 |

{ 14 | if (!rebootSelected) { 15 | hardwareStore.reboot(); 16 | setRebootSelected(true); 17 | } 18 | }} 19 | > 20 |
请再试一次
21 |
22 | Car Thing 无法完成更新,请确保您的手机具备网络链接后点击屏幕任意位置进行重试。 23 |
24 |
25 | ); 26 | }; 27 | 28 | export default Failed; 29 | -------------------------------------------------------------------------------- /component/Onboarding/DialPressPulse.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .outerCircle { 4 | width: 432px; 5 | height: 432px; 6 | background: $white; 7 | z-index: $onboarding-tactile-z-index; 8 | position: absolute; 9 | left: 688px; 10 | top: -61px; 11 | border-radius: 50%; 12 | } 13 | 14 | .innerCircle { 15 | width: 300px; 16 | height: 300px; 17 | background: $green; 18 | z-index: $onboarding-tactile-z-index + 1; 19 | position: absolute; 20 | border-radius: 50%; 21 | animation-name: pulse; 22 | animation-duration: 1000ms; 23 | animation-iteration-count: infinite; 24 | animation-timing-function: linear; 25 | top: 65px; 26 | right: 65px; 27 | } 28 | 29 | @keyframes pulse { 30 | from { 31 | opacity: 1; 32 | transform: scale(1); 33 | } 34 | 35 | to { 36 | opacity: 0; 37 | transform: scale(1.44); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /component/Npv/OtherMedia/Widget/Widget.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .otherMediaWidget { 4 | margin-top: 20px; 5 | width: 720px; 6 | height: 224px; 7 | background-color: $gray-20; 8 | border-radius: 16px; 9 | display: flex; 10 | overflow: hidden; 11 | } 12 | 13 | .artwork { 14 | box-shadow: 0 16px 32px rgb(0 0 0 / 20%); 15 | } 16 | 17 | .metadata { 18 | width: 100%; 19 | padding: 12px 32px; 20 | display: flex; 21 | flex-direction: column; 22 | justify-content: space-evenly; 23 | overflow: hidden; 24 | } 25 | 26 | .source { 27 | color: $opacity-white-70; 28 | } 29 | 30 | .title { 31 | overflow: hidden; 32 | display: -webkit-box; 33 | -webkit-line-clamp: 2; 34 | -webkit-box-orient: vertical; 35 | } 36 | 37 | .subtitle { 38 | white-space: nowrap; 39 | overflow: hidden; 40 | text-overflow: ellipsis; 41 | color: white; 42 | } 43 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import path from 'path'; 4 | import { readdirSync } from 'fs'; 5 | import legacy from '@vitejs/plugin-legacy'; 6 | 7 | const absolutePathAliases: { [key: string]: string } = {}; 8 | // Root resources folder 9 | const srcPath = path.resolve('./'); 10 | const srcRootContent = readdirSync(srcPath, { withFileTypes: true }).map((dirent) => dirent.name.replace(/(\.ts|\.js)(x?)/, '')); 11 | 12 | srcRootContent.forEach((directory) => { 13 | absolutePathAliases[directory] = path.join(srcPath, directory); 14 | }); 15 | 16 | // https://vitejs.dev/config/ 17 | export default defineConfig({ 18 | resolve: { 19 | alias: { 20 | ...absolutePathAliases 21 | } 22 | }, 23 | plugins: [ 24 | legacy({ 25 | targets: ['Chrome 69'] 26 | }), 27 | react() 28 | ] 29 | }); 30 | -------------------------------------------------------------------------------- /@spotify-internal/encore-web/es/components/ButtonPrimary/ButtonChildren.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IconWrapper } from "./IconWrapper"; 3 | export var ButtonChildren = function ButtonChildren(_ref) { 4 | var iconOnly = _ref.iconOnly, 5 | iconLeading = _ref.iconLeading, 6 | iconTrailing = _ref.iconTrailing, 7 | children = _ref.children, 8 | buttonSize = _ref.buttonSize; 9 | 10 | var renderIcon = function renderIcon(position, icon) { 11 | return icon && /*#__PURE__*/React.createElement(IconWrapper, { 12 | icon: icon, 13 | position: position, 14 | buttonSize: buttonSize 15 | }); 16 | }; 17 | 18 | return iconOnly ? /*#__PURE__*/React.createElement(React.Fragment, null, renderIcon('only', iconOnly)) : /*#__PURE__*/React.createElement(React.Fragment, null, renderIcon('leading', iconLeading), children, renderIcon('trailing', iconTrailing)); 19 | }; -------------------------------------------------------------------------------- /@spotify-internal/encore-web/es/components/ButtonSecondary/ButtonChildren.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IconWrapper } from "./IconWrapper"; 3 | export var ButtonChildren = function ButtonChildren(_ref) { 4 | var iconOnly = _ref.iconOnly, 5 | iconLeading = _ref.iconLeading, 6 | iconTrailing = _ref.iconTrailing, 7 | children = _ref.children, 8 | buttonSize = _ref.buttonSize; 9 | 10 | var renderIcon = function renderIcon(position, icon) { 11 | return icon && /*#__PURE__*/React.createElement(IconWrapper, { 12 | icon: icon, 13 | position: position, 14 | buttonSize: buttonSize 15 | }); 16 | }; 17 | 18 | return iconOnly ? /*#__PURE__*/React.createElement(React.Fragment, null, renderIcon('only', iconOnly)) : /*#__PURE__*/React.createElement(React.Fragment, null, renderIcon('leading', iconLeading), children, renderIcon('trailing', iconTrailing)); 19 | }; -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Superbird 10 | 11 | 12 | 13 | 14 |
appstart
16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /component/Modals/BluetoothPairing.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import styles from './Modal.module.scss'; 3 | import { useStore } from 'context/store'; 4 | import { SpotifyLogo } from 'component/CarthingUIComponents'; 5 | 6 | const BluetoothPairing = () => { 7 | const { bluetoothStore, ubiLogger } = useStore(); 8 | useEffect( 9 | () => ubiLogger.modalUbiLogger.logBluetoothPinPairingImpression(), 10 | [ubiLogger.modalUbiLogger], 11 | ); 12 | 13 | return ( 14 |
15 | 16 |
配对中...
17 |
18 | 确认您在手机上看到以下代码。 19 |
20 |
{bluetoothStore.pin}
21 |
22 | ); 23 | }; 24 | 25 | export default BluetoothPairing; 26 | -------------------------------------------------------------------------------- /component/Modals/ModalContent.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .errorModal { 4 | height: 365px; 5 | width: 560px; 6 | } 7 | 8 | .modalRoot { 9 | display: flex; 10 | flex-direction: column; 11 | align-items: center; 12 | 13 | .modalIcon { 14 | margin-top: 38px; 15 | transform: scale(2.5); 16 | } 17 | 18 | .iconNowPlaying { 19 | color: $green-light; 20 | } 21 | 22 | .modalTitle { 23 | margin-top: 23px; 24 | 25 | @include text-style(48px, 36px, -0.04em); 26 | 27 | font-weight: bold; 28 | color: $green-light; 29 | } 30 | 31 | .modalError { 32 | color: $red-light; 33 | } 34 | 35 | .modalText { 36 | margin-top: 24px; 37 | color: $black; 38 | 39 | @include text-style(32px, 40px, -0.02em); 40 | 41 | text-align: center; 42 | width: 485px; 43 | 44 | .boldText { 45 | font-weight: bold; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /component/Modals/PremiumAccountRequired.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import styles from './Modal.module.scss'; 3 | 4 | import { SpotifyLogo } from 'component/CarthingUIComponents'; 5 | import { useStore } from 'context/store'; 6 | 7 | const PremiumAccountRequired = () => { 8 | const { ubiLogger } = useStore(); 9 | useEffect( 10 | () => ubiLogger.modalUbiLogger.logNeedPremiumModalShown(), 11 | [ubiLogger.modalUbiLogger], 12 | ); 13 | return ( 14 |
15 | 16 |
需要高级账户
17 |
18 |

19 | 要使用 Car Thing,您需要在手机上登录 Spotify Premium 或 Premium Family 帐户。 20 |

21 |
22 |
23 | ); 24 | }; 25 | 26 | export default PremiumAccountRequired; 27 | -------------------------------------------------------------------------------- /component/Npv/ControlButtons/Controls.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .controls { 4 | width: 800px; 5 | display: flex; 6 | align-items: center; 7 | } 8 | 9 | .otherMedia { 10 | background-color: $gray-20; 11 | } 12 | 13 | .controlButton { 14 | height: 144px; 15 | width: 160px; 16 | display: flex; 17 | justify-content: center; 18 | align-items: center; 19 | } 20 | 21 | .touchArea { 22 | width: 144px; 23 | height: 112px; 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | border-radius: 8px; 28 | } 29 | 30 | .touchAreaFullSize { 31 | width: 100%; 32 | height: 100%; 33 | border-radius: unset; 34 | } 35 | 36 | .touchAreaDown { 37 | background: $opacity-black-30; 38 | } 39 | 40 | .iconShuffleActive { 41 | padding-top: 23px; 42 | } 43 | 44 | .disabledIcon { 45 | svg { 46 | path { 47 | fill: $opacity-white-30; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /component/Setup/Welcome.tsx: -------------------------------------------------------------------------------- 1 | import { IconArrowRight } from '@spotify-internal/encore-web'; 2 | import { useStore } from 'context/store'; 3 | import { SetupView } from 'store/SetupStore'; 4 | import styles from './Welcome.module.scss'; 5 | 6 | const Welcome = () => { 7 | const { setupStore } = useStore(); 8 | return ( 9 |
10 |
嗨!欢迎使用 Car Thing。
11 |
12 |
马上开始
13 |
18 | 19 |
20 |
21 |
22 | ); 23 | }; 24 | 25 | export default Welcome; 26 | -------------------------------------------------------------------------------- /component/Setup/Setup.tsx: -------------------------------------------------------------------------------- 1 | import { observer } from 'mobx-react-lite'; 2 | import StartSetup from './StartSetup'; 3 | import Connected from './Connected'; 4 | import BTPairing from './BTPairing'; 5 | import Welcome from './Welcome'; 6 | import Updating from './Updating'; 7 | import Failed from './Failed'; 8 | import Waiting from './Waiting'; 9 | import { SetupView } from 'store/SetupStore'; 10 | import { useStore } from 'context/store'; 11 | 12 | const viewToComp = { 13 | [SetupView.WELCOME]: , 14 | [SetupView.START_SETUP]: , 15 | [SetupView.BT_PAIRING]: , 16 | [SetupView.CONNECTED]: , 17 | [SetupView.UPDATING]: , 18 | [SetupView.FAILED]: , 19 | [SetupView.WAITING]: , 20 | }; 21 | 22 | const Setup = () => { 23 | const { setupStore } = useStore(); 24 | 25 | return viewToComp[setupStore.currentStep]; 26 | }; 27 | 28 | export default observer(Setup); 29 | -------------------------------------------------------------------------------- /component/Presets/PresetCard/PresetCard.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .presetCard { 4 | width: 200px; 5 | margin: 0 auto; 6 | @include transition-opacity-transform(400ms, $advance-default-cubic); 7 | 8 | will-change: transform; 9 | display: flex; 10 | flex-direction: column; 11 | align-items: center; 12 | } 13 | 14 | .appear { 15 | transform: translateY(-350px); 16 | opacity: 0; 17 | } 18 | 19 | .appearActive { 20 | transform: translateY(0); 21 | opacity: 1; 22 | 23 | &.presetCard1 { 24 | transition-delay: 0ms; 25 | } 26 | 27 | &.presetCard2 { 28 | transition-delay: 50ms; 29 | } 30 | 31 | &.presetCard3 { 32 | transition-delay: 100ms; 33 | } 34 | 35 | &.presetCard4 { 36 | transition-delay: 150ms; 37 | } 38 | } 39 | 40 | .exit { 41 | transform: translateY(0); 42 | opacity: 1; 43 | } 44 | 45 | .exitActive { 46 | transform: translateY(-350px); 47 | opacity: 0; 48 | } 49 | -------------------------------------------------------------------------------- /@spotify-internal/encore-web/es/contexts/EncoreContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | export var encoreContextStatus = { 3 | experimental: 'experimental', 4 | next: 'next', 5 | deprecated: 'deprecated' 6 | }; 7 | export var encoreContextKeyword = { 8 | button: 'button' 9 | }; 10 | export var EncoreContextDefault = { 11 | experimental: [], 12 | next: [], 13 | deprecated: [] 14 | }; 15 | export var hasStatus = function hasStatus(keyword, status) { 16 | return status.indexOf(keyword) > -1; 17 | }; 18 | export var getStatus = function getStatus(keyword, config) { 19 | var lifecycle = undefined; 20 | var statusKeys = Object.keys(encoreContextStatus); 21 | statusKeys.forEach(function (status) { 22 | if (hasStatus(keyword, config[status])) lifecycle = status; 23 | }); 24 | return lifecycle; 25 | }; 26 | var EncoreContext = /*#__PURE__*/React.createContext(EncoreContextDefault); 27 | EncoreContext.displayName = 'Encore'; 28 | export { EncoreContext }; -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconPhoneAnswer.tsx: -------------------------------------------------------------------------------- 1 | const IconPhoneAnswer = () => ( 2 | 9 | 13 | 14 | ); 15 | 16 | export default IconPhoneAnswer; 17 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Icons/IconPhoneDecline.tsx: -------------------------------------------------------------------------------- 1 | const IconPhoneDecline = () => ( 2 | 9 | 13 | 14 | ); 15 | 16 | export default IconPhoneDecline; 17 | -------------------------------------------------------------------------------- /component/Tracklist/EmptyTracklistState.tsx: -------------------------------------------------------------------------------- 1 | import styles from './Tracklist.module.scss'; 2 | import { isPlaylistV1OrV2URI } from '@spotify-internal/uri'; 3 | import { 4 | isCollectionUri, 5 | isNewEpisodesUri, 6 | isYourEpisodesUri, 7 | } from 'helpers/SpotifyUriUtil'; 8 | 9 | const getTextBasedOnUri = (uri: string): string => { 10 | if (isPlaylistV1OrV2URI(uri)) { 11 | return '没有歌曲添加到此歌单'; 12 | } else if (isCollectionUri(uri)) { 13 | if (isYourEpisodesUri(uri)) { 14 | return "你收集的播客单集在这里。"; 15 | } else if (isNewEpisodesUri(uri)) { 16 | return '一旦你关注了某个节目,这里就会出现新单集提醒。'; 17 | } 18 | return '您喜欢的歌曲将出现在此处。'; 19 | } 20 | 21 | return ''; 22 | }; 23 | 24 | const EmptyTracklistState = ({ contextUri }: { contextUri: string }) => { 25 | return ( 26 |
27 |

{getTextBasedOnUri(contextUri)}

28 |
29 | ); 30 | }; 31 | 32 | export default EmptyTracklistState; 33 | -------------------------------------------------------------------------------- /component/PhoneCall/PhoneCall.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .phoneCall { 4 | height: $device-height; 5 | position: relative; 6 | width: $device-width; 7 | display: flex; 8 | flex-direction: column; 9 | } 10 | 11 | .infoWrapper { 12 | width: 100%; 13 | height: 345px; 14 | display: flex; 15 | flex-direction: column; 16 | align-items: center; 17 | padding-top: 58px; 18 | 19 | img { 20 | width: 64px; 21 | height: 64px; 22 | border-radius: 50%; 23 | margin-top: 0; 24 | } 25 | 26 | svg, 27 | img { 28 | margin-bottom: 24px; 29 | } 30 | 31 | .title, 32 | .subtitle { 33 | margin-bottom: 16px; 34 | width: 624px; 35 | text-align: center; 36 | overflow: hidden; 37 | text-overflow: ellipsis; 38 | white-space: nowrap; 39 | } 40 | 41 | .subtitle { 42 | height: 40px; 43 | } 44 | } 45 | 46 | .actions { 47 | width: 100%; 48 | height: 148px; 49 | display: flex; 50 | margin-left: 173px; 51 | } 52 | -------------------------------------------------------------------------------- /component/Onboarding/Start.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .start { 4 | height: $device-height; 5 | width: $device-width; 6 | background-color: $pink; 7 | display: flex; 8 | align-items: center; 9 | justify-content: space-between; 10 | flex-direction: column; 11 | padding: 32px 40px; 12 | } 13 | 14 | .title { 15 | @include text-style(120px, 120px, -0.04em); 16 | 17 | color: $aubergine; 18 | } 19 | 20 | .tourAction { 21 | width: 100%; 22 | display: flex; 23 | align-items: center; 24 | justify-content: space-between; 25 | 26 | .subtitle { 27 | @include text-style(72px, 72px, -0.03em); 28 | 29 | color: $aubergine; 30 | } 31 | 32 | .arrowWrapper { 33 | display: flex; 34 | align-items: center; 35 | justify-content: center; 36 | width: 88px; 37 | height: 88px; 38 | border-radius: 50%; 39 | background: $aubergine; 40 | 41 | svg { 42 | transform: scale(3); 43 | color: $pink; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /@spotify-internal/encore-web/es/styles/variables.js: -------------------------------------------------------------------------------- 1 | import { opacity30, opacity70, spacer24, spacer64 } from '@spotify-internal/encore-foundation'; // Typography 2 | 3 | export var fontWeightBook = 400; 4 | export var fontWeightBold = 700; 5 | export var fontWeightBlack = 900; // z-index master list 6 | 7 | export var zIndexPreviewBackdrop = 0; 8 | export var zIndexBannerDecorator = 1; 9 | export var zIndexFixed = 1030; 10 | export var zIndexDialogBackdrop = 1040; 11 | export var zIndexDialog = 1050; 12 | export var zIndexPopover = 1060; 13 | export var zIndexSkipLink = 9999; // Sidebar 14 | 15 | export var sidebarBaseWidth = '200px'; 16 | export var sidebarSlimBaseWidth = spacer64; 17 | export var sidebarPadding = spacer24; // Transitions + Animations 18 | 19 | export var transitionFade = '0.1s'; // Opacities 20 | 21 | export var opacityDisabled = opacity30; 22 | export var opacityActive = opacity70; // Misc 23 | 24 | export var cursorDisabled = 'not-allowed'; // Disabled cursor for form controls and buttons. -------------------------------------------------------------------------------- /component/Setup/BTPairing.tsx: -------------------------------------------------------------------------------- 1 | import styles from './BTPairing.module.scss'; 2 | import { observer } from 'mobx-react-lite'; 3 | import { SetupView } from 'store/SetupStore'; 4 | import { useStore } from 'context/store'; 5 | 6 | const parsePincode = (pinCode?: string) => { 7 | if (!pinCode) return pinCode; 8 | 9 | return pinCode.split('').join(' '); 10 | }; 11 | const BTPairing = () => { 12 | const { bluetoothStore } = useStore(); 13 | return ( 14 |
18 |
配对代码
19 |
20 |
21 | 在 Spotify 应用内确认您已看见配对代码。 22 |
23 |
24 | {parsePincode(bluetoothStore.bluetoothPairingPin)} 25 |
26 |
27 |
28 | ); 29 | }; 30 | 31 | export default observer(BTPairing); 32 | -------------------------------------------------------------------------------- /component/NightMode/NightModeUiState.ts: -------------------------------------------------------------------------------- 1 | import { makeAutoObservable } from 'mobx'; 2 | 3 | import HardwareStore from 'store/HardwareStore'; 4 | import RemoteConfigStore from 'store/RemoteConfigStore'; 5 | 6 | export type NightModeUiState = ReturnType; 7 | 8 | const roundToTwoDecimals = (n: number) => 9 | Math.round((n + Number.EPSILON) * 100) / 100; 10 | 11 | const nightModeUiStateFactory = ( 12 | hardwareStore: HardwareStore, 13 | remoteConfigStore: RemoteConfigStore, 14 | ) => { 15 | return makeAutoObservable({ 16 | get appOpacity(): number { 17 | return roundToTwoDecimals( 18 | 1 - 19 | (this.nightModeSlope * hardwareStore.ambientLightValue + 20 | remoteConfigStore.nightModeStrength - 21 | 100) / 22 | 100, 23 | ); 24 | }, 25 | 26 | get nightModeSlope(): number { 27 | return remoteConfigStore.nightModeSlope / 10; 28 | }, 29 | }); 30 | }; 31 | 32 | export default nightModeUiStateFactory; 33 | -------------------------------------------------------------------------------- /component/Settings/UnavailableSettingBanner/UnavailableSettingBanner.tsx: -------------------------------------------------------------------------------- 1 | import Banner from 'component/CarthingUIComponents/Banner/Banner'; 2 | import { useStore } from 'context/store'; 3 | import { observer } from 'mobx-react-lite'; 4 | import { useEffect } from 'react'; 5 | import { runInAction } from 'mobx'; 6 | import { IconInfo32 } from 'component/CarthingUIComponents'; 7 | 8 | const UnavailableSettingBanner = () => { 9 | const uiState = useStore().settingsStore.unavailableSettingsBannerUiState; 10 | 11 | useEffect(() => { 12 | runInAction(() => { 13 | if (uiState.shouldShowAlert) { 14 | uiState.logImpression(); 15 | } 16 | }); 17 | }, [uiState]); 18 | 19 | const infoText = '抱歉,部分配置在当前情况下不可用。'; 20 | 21 | const icon = ; 22 | 23 | return ( 24 | 30 | ); 31 | }; 32 | 33 | export default observer(UnavailableSettingBanner); 34 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | tags: 5 | - '**' 6 | jobs: 7 | build: 8 | name: Build 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: mskelton/setup-yarn@v1 13 | with: 14 | node-version: '18.12.1' 15 | - name: Run build 16 | run: yarn build 17 | - name: Get release 18 | id: get_release 19 | uses: bruceadams/get-release@v1.3.2 20 | env: 21 | GITHUB_TOKEN: ${{ github.token }} 22 | - uses: montudor/action-zip@v1 23 | with: 24 | args: zip -qq -r release.zip dist push.sh push.bat 25 | - name: Upload artifact 26 | uses: actions/upload-release-asset@v1 27 | env: 28 | GITHUB_TOKEN: ${{ github.token }} 29 | with: 30 | upload_url: ${{ steps.get_release.outputs.upload_url }} 31 | asset_path: ./release.zip 32 | asset_name: release.zip 33 | asset_content_type: application/zip 34 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Spinner/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import styles from './Spinner.module.scss'; 2 | 3 | export enum SpinnerSize { 4 | SMALL, 5 | BIG, 6 | } 7 | 8 | export type Props = { 9 | size: SpinnerSize; 10 | }; 11 | 12 | const sizeToAttributes = { 13 | [SpinnerSize.SMALL]: { sideLength: 32, strokeWidth: 25 }, 14 | [SpinnerSize.BIG]: { sideLength: 104, strokeWidth: 15 }, 15 | }; 16 | 17 | const Spinner = ({ size }: Props) => ( 18 |
25 | 30 | 37 | 38 |
39 | ); 40 | 41 | export default Spinner; 42 | -------------------------------------------------------------------------------- /component/Settings/RestartConfirm/RestartConfirm.tsx: -------------------------------------------------------------------------------- 1 | import { useStore } from 'context/store'; 2 | import { useEffect } from 'react'; 3 | import styles from './RestartConfirm.module.scss'; 4 | import { Button, ButtonType } from 'component/CarthingUIComponents'; 5 | 6 | const RestartConfirm = () => { 7 | const { 8 | hardwareStore, 9 | ubiLogger: { settingsUbiLogger }, 10 | } = useStore(); 11 | 12 | useEffect(() => { 13 | settingsUbiLogger.logRestartConfirmDialogImpression(); 14 | }, [settingsUbiLogger]); 15 | 16 | const reboot = () => { 17 | settingsUbiLogger.logRestartConfirmButtonClick(); 18 | hardwareStore.reboot(); 19 | }; 20 | 21 | return ( 22 |
23 |
重启
24 |
25 | 您确定要重启设备吗? 26 |
27 | 30 |
31 | ); 32 | }; 33 | 34 | export default RestartConfirm; 35 | -------------------------------------------------------------------------------- /component/SwipeDownHandle/SwipeDownHandleUiState.ts: -------------------------------------------------------------------------------- 1 | import PresetsController from 'component/Presets/PresetsController'; 2 | import PresetsUbiLogger from 'eventhandler/PresetsUbiLogger'; 3 | import { OverlayController } from 'component/Overlays/OverlayController'; 4 | 5 | class SwipeDownHandleUiState { 6 | overlayController: OverlayController; 7 | presetsStore: PresetsController; 8 | presetsUbiLogger: PresetsUbiLogger; 9 | 10 | constructor( 11 | overlayController: OverlayController, 12 | presetsStore: PresetsController, 13 | presetsUbiLogger: PresetsUbiLogger, 14 | ) { 15 | this.overlayController = overlayController; 16 | this.presetsStore = presetsStore; 17 | this.presetsUbiLogger = presetsUbiLogger; 18 | } 19 | 20 | onSwipeDown = () => { 21 | if (this.presetsStore.isSwipeDownPresetsEnabled) { 22 | this.presetsUbiLogger.logSwipeDown(); 23 | this.overlayController.showPresets(); 24 | this.presetsStore.presetsUiState.highlightPreset(); 25 | } 26 | }; 27 | } 28 | 29 | export default SwipeDownHandleUiState; 30 | -------------------------------------------------------------------------------- /component/Setup/StartSetup.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .screen { 4 | background: $aubergine; 5 | height: $device-height; 6 | width: $device-width; 7 | padding: 32px 48px 40px; 8 | } 9 | 10 | .title { 11 | color: $pink; 12 | 13 | @include text-style(120px, 120px, -0.04em); 14 | 15 | width: 704px; 16 | } 17 | 18 | .subtitle { 19 | color: $pink; 20 | 21 | @include text-style(36px, 48px, -0.02em); 22 | 23 | max-width: 408px; 24 | } 25 | 26 | .content { 27 | margin-top: 32px; 28 | width: 704px; 29 | display: flex; 30 | justify-content: space-between; 31 | } 32 | 33 | .texts { 34 | display: flex; 35 | flex-direction: column; 36 | justify-content: space-between; 37 | } 38 | 39 | .qrContainer { 40 | height: 256px; 41 | width: 256px; 42 | background-color: white; 43 | border-radius: 24px; 44 | display: flex; 45 | align-items: center; 46 | justify-content: center; 47 | } 48 | 49 | .needHelp { 50 | color: $pink; 51 | 52 | @include text-style(36px, 40px, -0.02em); 53 | 54 | font-weight: bold; 55 | } 56 | -------------------------------------------------------------------------------- /public/images/mobile-signal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /component/Npv/ControlButtons/SaveEpisode.tsx: -------------------------------------------------------------------------------- 1 | import { IconAddAlt48, IconCheckAlt } from 'component/CarthingUIComponents'; 2 | import { NpvIcon } from 'component/Npv/ControlButtons/Controls'; 3 | import { useStore } from 'context/store'; 4 | import { observer } from 'mobx-react-lite'; 5 | import ControlButton from './ControlButton'; 6 | 7 | const SaveEpisode = () => { 8 | const uiState = useStore().npvStore.controlButtonsUiState; 9 | 10 | return ( 11 | <> 12 | {uiState.isSaved ? ( 13 | 17 | 18 | 19 | ) : ( 20 | 25 | 26 | 27 | )} 28 | 29 | ); 30 | }; 31 | 32 | export default observer(SaveEpisode); 33 | -------------------------------------------------------------------------------- /component/PhoneCall/AnswerButton.tsx: -------------------------------------------------------------------------------- 1 | import { observer } from 'mobx-react-lite'; 2 | import styles from 'component/PhoneCall/AnswerButton.module.scss'; 3 | import { 4 | Button, 5 | ButtonType, 6 | IconPhoneAnswer, 7 | } from 'component/CarthingUIComponents'; 8 | import { useStore } from 'context/store'; 9 | import classNames from 'classnames'; 10 | 11 | const AnswerButton = () => { 12 | const uiState = useStore().phoneCallController.phoneCallUiState; 13 | 14 | return ( 15 | 30 | ); 31 | }; 32 | 33 | export default observer(AnswerButton); 34 | -------------------------------------------------------------------------------- /component/Setup/Welcome.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .screen { 4 | height: $device-height; 5 | width: $device-width; 6 | background-color: $pink; 7 | display: flex; 8 | align-items: center; 9 | justify-content: space-between; 10 | flex-direction: column; 11 | } 12 | 13 | .title { 14 | color: $aubergine; 15 | margin-top: 32px; 16 | margin-left: 40px; 17 | 18 | @include text-style(120px, 120px, -0.04em); 19 | } 20 | 21 | .subtitle { 22 | color: $aubergine; 23 | 24 | @include text-style(72px, 72px, -0.03em); 25 | } 26 | 27 | .buttonBackground { 28 | background: $aubergine; 29 | display: flex; 30 | align-items: center; 31 | justify-content: space-around; 32 | width: 88px; 33 | height: 88px; 34 | border-radius: 50%; 35 | } 36 | 37 | .button { 38 | transform: scale(3); 39 | color: $pink; 40 | } 41 | 42 | .subtitleAndButton { 43 | width: 720px; 44 | margin-left: 40px; 45 | margin-right: 48px; 46 | margin-bottom: 28px; 47 | display: flex; 48 | align-items: center; 49 | justify-content: space-between; 50 | } 51 | -------------------------------------------------------------------------------- /public/license/i18n-license.txt: -------------------------------------------------------------------------------- 1 | 简体中文本地化 2 | 3 | https://github.com/NekoLines/Spotify-Car-Thing-Chinese-Webapps 4 | 5 | 锦鲤@NekoLines,立音喵@CubeSky 6 | 7 | --- 8 | 9 | 本代码在编译后按照原样提供,没有任何基于任何原因的明示或暗示。 10 | 在任何情景,任何情况下,作者都不对该代码造成的直接或间接损失/损害承担任何责任。 11 | 12 | 允许任何人在非盈利,非商业的情况下使用本软件或对本软件进行修改并在不设任何前提条件 13 | 的情况下对本项目进行分发。 14 | 15 | 请遵守以下限制: 16 | 17 | 1.不得扭曲本项目的来源,您不能宣称您自己编写了本项目,若您使用了本项目,必须在您的 18 | 产品中保留本人的名称(锦鲤@NekoLines(https://github.com/NekoLines))。 19 | 20 | 2.更改的部分必须在您的项目中进行明确标注,且该部分更改不得被误解为原始代码。 21 | 22 | 3.本项目当前说明不得在任何来源的分发中删除或更改。 23 | 24 | 4.即便您进行了修改,也不允许在分发时增设任何条件(包括但不限于关注公众号,私信,邮箱,收费等)。 25 | 26 | 在以上限制之外,本程序遵循 AGPLv3 协议进行分发。 27 | 28 | --- 29 | 30 | 本汉化固件在制作过程中使用了以下资源: 31 | https://github.com/frederic/superbird-bulkcmd.git(工具包), 32 | https://github.com/bishopdynamics/superbird-tool.git(工具包), 33 | https://github.com/Merlin04/superbird-webapp.git(程序源代码), 34 | https://github.com/err4o4/spotify-car-thing-reverse-engineering.git(经过修改的系统固件镜像,具体而言请参考 35 | https://github.com/err4o4/spotify-car-thing-reverse-engineering/issues/22#issue-1432896381) 36 | 以上项目的协议请您单独确认。 37 | 38 | --- 39 | -------------------------------------------------------------------------------- /component/Npv/Scrubbing/ScrubbingBackdrop.tsx: -------------------------------------------------------------------------------- 1 | import Overlay, { FROM } from 'component/Overlays/Overlay'; 2 | import { useStore } from 'context/store'; 3 | import { observer } from 'mobx-react-lite'; 4 | import './ScrubbingBackdrop.module.scss'; 5 | import styles from './ScrubbingBackdrop.module.scss'; 6 | 7 | const ProgressBackdrop = () => { 8 | const uiState = useStore().npvStore.scrubbingUiState; 9 | 10 | return ( 11 | 12 |
19 |
20 | {uiState.trackPlayedTime} 21 | - {uiState.trackLeftTime} 22 |
23 |
24 |
25 | ); 26 | }; 27 | 28 | export default observer(ProgressBackdrop); 29 | -------------------------------------------------------------------------------- /component/Tracklist/Tracklist.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | $dial-center-diff: 68px; 4 | $small-header-diff: 48px; 5 | 6 | .tracklist { 7 | @include transition-transform; 8 | 9 | width: $device-width; 10 | height: $device-height + $small-header-diff; 11 | position: relative; 12 | top: 0; 13 | left: 0; 14 | overflow: hidden; 15 | 16 | &.smallHeader { 17 | transform: translateY(-$small-header-diff); 18 | } 19 | } 20 | 21 | .container { 22 | position: relative; 23 | left: 0; 24 | right: 0; 25 | width: 100%; 26 | height: $device-height + $dial-center-diff; 27 | overflow: hidden; 28 | z-index: $overlay-z-index; 29 | } 30 | 31 | .emptyBody { 32 | width: 100%; 33 | height: 320px; 34 | display: flex; 35 | align-items: center; 36 | justify-content: center; 37 | 38 | p { 39 | width: 560px; 40 | text-align: center; 41 | transform: translateY(-48px); 42 | 43 | @include text-style(28px, 32px, -0.01em); 44 | 45 | color: $gray-70; 46 | } 47 | } 48 | 49 | .trackSlideScrolled { 50 | transform: translateY(48px); 51 | } 52 | -------------------------------------------------------------------------------- /Main.tsx: -------------------------------------------------------------------------------- 1 | import Views from 'component/Views/Views'; 2 | import { useStore } from './context/store'; 3 | import { useEffect } from 'react'; 4 | import SwipeDownHandle from 'component/SwipeDownHandle/SwipeDownHandle'; 5 | import Overlays from 'component/Overlays/Overlays'; 6 | import { action } from 'mobx'; 7 | 8 | const Main = () => { 9 | const { npvStore, shelfStore, overlayController, viewStore } = useStore(); 10 | 11 | useEffect(() => { 12 | setTimeout(() => overlayController.maybeShowAModal(), 2000); 13 | }, [overlayController]); 14 | 15 | const handlePointerDown = action(() => { 16 | if (viewStore.isNpv && !overlayController.anyOverlayIsShowing) { 17 | npvStore.tipsUiState.dismissVisibleTip(); 18 | } 19 | }); 20 | 21 | const handleClick = action(() => { 22 | shelfStore.shelfController.voiceMuteBannerUiState.dismissVoiceBanner(); 23 | }); 24 | 25 | return ( 26 |
27 | 28 | 29 | 30 |
31 | ); 32 | }; 33 | 34 | export default Main; 35 | -------------------------------------------------------------------------------- /component/Npv/ControlButtons/LikeTrack.tsx: -------------------------------------------------------------------------------- 1 | import { IconHeart48, IconHeartActive48 } from 'component/CarthingUIComponents'; 2 | import { NpvIcon } from 'component/Npv/ControlButtons/Controls'; 3 | import { useStore } from 'context/store'; 4 | import { observer } from 'mobx-react-lite'; 5 | import ControlButton from './ControlButton'; 6 | 7 | const LikeTrack = () => { 8 | const uiState = useStore().npvStore.controlButtonsUiState; 9 | const { playerStore } = useStore(); 10 | return ( 11 | <> 12 | {uiState.isSaved ? ( 13 | 18 | 19 | 20 | ) : ( 21 | 26 | 27 | 28 | )} 29 | 30 | ); 31 | }; 32 | 33 | export default observer(LikeTrack); 34 | -------------------------------------------------------------------------------- /component/Npv/PlayingInfoOrTip/PlayingInfoOrTip.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .playingInfoOrTip { 4 | height: 332px; 5 | width: 100%; 6 | } 7 | 8 | .playingInfoEnter { 9 | opacity: 0; 10 | transform: translateY(40px); 11 | } 12 | 13 | .playingInfoEnterActive { 14 | @include transition-opacity-transform(300ms, ease-in); 15 | 16 | opacity: 1; 17 | transform: translateY(0); 18 | } 19 | 20 | .playingInfoExit { 21 | opacity: 1; 22 | } 23 | 24 | .playingInfoExitActive { 25 | @include transition-opacity-transform(300ms, ease-out); 26 | 27 | opacity: 0; 28 | transform: translateY(40px); 29 | } 30 | 31 | .tipEnter { 32 | opacity: 0; 33 | transform: translateY(72px); 34 | } 35 | 36 | .tipEnterActive { 37 | @include transition-opacity-transform(300ms, ease-in); 38 | 39 | opacity: 1; 40 | transform: translateY(0); 41 | } 42 | 43 | .tipExit { 44 | opacity: 1; 45 | transform: translateY(0); 46 | } 47 | 48 | .tipExitActive { 49 | @include transition-opacity-transform(300ms, ease-out); 50 | 51 | opacity: 0; 52 | transform: translateY(72px); 53 | } 54 | -------------------------------------------------------------------------------- /component/Main.tsx: -------------------------------------------------------------------------------- 1 | import Views from 'component/Views/Views'; 2 | import { useStore } from 'context/store'; 3 | import { useEffect } from 'react'; 4 | import SwipeDownHandle from 'component/SwipeDownHandle/SwipeDownHandle'; 5 | import Overlays from 'component/Overlays/Overlays'; 6 | import { action } from 'mobx'; 7 | 8 | const Main = () => { 9 | const { npvStore, shelfStore, overlayController, viewStore } = useStore(); 10 | 11 | useEffect(() => { 12 | setTimeout(() => overlayController.maybeShowAModal(), 2000); 13 | }, [overlayController]); 14 | 15 | const handlePointerDown = action(() => { 16 | if (viewStore.isNpv && !overlayController.anyOverlayIsShowing) { 17 | npvStore.tipsUiState.dismissVisibleTip(); 18 | } 19 | }); 20 | 21 | const handleClick = action(() => { 22 | shelfStore.shelfController.voiceMuteBannerUiState.dismissVoiceBanner(); 23 | }); 24 | 25 | return ( 26 |
27 | 28 | 29 | 30 |
31 | ); 32 | }; 33 | 34 | export default Main; 35 | -------------------------------------------------------------------------------- /component/Queue/QueueHeader/QueueHeader.tsx: -------------------------------------------------------------------------------- 1 | import { observer } from 'mobx-react-lite'; 2 | import styles from './QueueHeader.module.scss'; 3 | import classNames from 'classnames'; 4 | import { useStore } from 'context/store'; 5 | import Type from 'component/CarthingUIComponents/Type/Type'; 6 | 7 | const QueueHeader = () => { 8 | const uiState = useStore().queueStore.queueUiState; 9 | 10 | return ( 11 |
18 |
23 | 29 | {uiState.headerText} 30 | 31 |
32 |
33 | ); 34 | }; 35 | 36 | export default observer(QueueHeader); 37 | -------------------------------------------------------------------------------- /@spotify-internal/encore-web/es/styles/global-styles.js: -------------------------------------------------------------------------------- 1 | import _taggedTemplateLiteral from "@babel/runtime/helpers/esm/taggedTemplateLiteral"; 2 | 3 | var _templateObject; 4 | 5 | // 6 | // Global styles: Optional styles to reset CSS for an application 7 | // -------------------------------------------------------------- 8 | import { desktopBallad } from '@spotify-internal/encore-foundation'; 9 | import { createGlobalStyle } from 'styled-components'; 10 | export default createGlobalStyle(_templateObject || (_templateObject = _taggedTemplateLiteral(["\n /*\n Reset the box-sizing\n\n Heads up! This reset may cause conflicts with some third-party widgets.\n For recommendations on resolving such conflicts, see\n http://getbootstrap.com/getting-started/#third-box-sizing\n */\n\n * {\n box-sizing: border-box;\n }\n\n *::before,\n *::after {\n box-sizing: border-box;\n }\n\n /* Body reset */\n\n body {\n margin: 0;\n }\n\n body, input, textarea, button {\n font-family: var(--font-family, ", "), Helvetica, Arial, sans-serif;\n }\n\n html,\n body {\n height: 100%;\n }\n"])), desktopBallad.fontFamily); -------------------------------------------------------------------------------- /component/Promo/Promo.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .promo { 4 | width: 800px; 5 | height: 480px; 6 | display: flex; 7 | } 8 | 9 | .textAndConfirm { 10 | background: $white; 11 | height: 100%; 12 | width: 422px; 13 | padding: 40px 0 32px 40px; 14 | display: flex; 15 | justify-content: space-between; 16 | flex-direction: column; 17 | } 18 | 19 | .text { 20 | color: $aubergine; 21 | max-width: 328px; 22 | } 23 | 24 | .subText { 25 | color: $aubergine; 26 | max-width: 315px; 27 | } 28 | 29 | .confirmButton { 30 | background-color: $aubergine; 31 | color: $white; 32 | } 33 | 34 | .imageAndOption { 35 | background: $pink; 36 | height: 100%; 37 | width: 378px; 38 | display: flex; 39 | flex-direction: column; 40 | } 41 | 42 | .image { 43 | border-radius: 16px; 44 | width: 458px; 45 | height: 274.8px; 46 | box-shadow: 0 39px 44px -30px rgb(0 0 0 / 80%); 47 | margin-top: 56px; 48 | margin-left: -30px; 49 | } 50 | 51 | .optionButtonWrapper { 52 | width: 100%; 53 | margin-top: 29px; 54 | display: flex; 55 | justify-content: flex-end; 56 | padding-right: 40px; 57 | } 58 | -------------------------------------------------------------------------------- /public/images/no-connection.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /component/Npv/OtherMedia/BackToSpotify/BackToSpotify.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import { SpotifyLogo } from 'component/CarthingUIComponents'; 3 | import Type from 'component/CarthingUIComponents/Type/Type'; 4 | import { useStore } from 'context/store'; 5 | import pointerListeners from 'helpers/PointerListeners'; 6 | import { observer } from 'mobx-react-lite'; 7 | import { useState } from 'react'; 8 | import styles from './BackToSpotify.module.scss'; 9 | 10 | const BackToSpotify = () => { 11 | const [isPressed, setIsPressed] = useState(false); 12 | 13 | const controller = useStore().npvStore.otherMediaController; 14 | 15 | return ( 16 |
23 | 24 | 25 | 返回 Spotify 26 | 27 |
28 | ); 29 | }; 30 | 31 | export default observer(BackToSpotify); 32 | -------------------------------------------------------------------------------- /@spotify-internal/uri/src/_internal/helpers.js: -------------------------------------------------------------------------------- 1 | import { URIFormat } from '../enums/uri_format'; 2 | /** 3 | * Encodes a component according to a URIformat. 4 | * 5 | * @param component - A component string. 6 | * @param format - A URIformat. 7 | * @return An encoded component string. 8 | */ 9 | export function encodeComponent(component, format) { 10 | if (!component) { 11 | return ''; 12 | } 13 | let encodedComponent = encodeURIComponent(component); 14 | if (format === URIFormat.URI) { 15 | encodedComponent = encodedComponent.replace(/%20/g, '+'); 16 | } 17 | // encode characters that are not encoded by default by encodeURIComponent 18 | // but that the Spotify URI spec encodes: !'*() 19 | encodedComponent = encodedComponent.replace(/[!'()]/g, escape); 20 | encodedComponent = encodedComponent.replace(/\*/g, '%2A'); 21 | return encodedComponent; 22 | } 23 | export function decodeComponent(component, format) { 24 | if (!component) { 25 | return ''; 26 | } 27 | const part = format === URIFormat.URI ? component.replace(/\+/g, '%20') : component; 28 | return decodeURIComponent(part); 29 | } 30 | -------------------------------------------------------------------------------- /component/AmbientBackdrop/AmbientBackdrop.tsx: -------------------------------------------------------------------------------- 1 | import { useStore } from 'context/store'; 2 | import { get } from 'mobx'; 3 | import { observer } from 'mobx-react-lite'; 4 | import { useRef } from 'react'; 5 | import styles from './AmbientBackdrop.module.scss'; 6 | 7 | type Props = { 8 | imageId?: string; 9 | getBackgroundStyleAttribute: (currentColor: number[]) => string; 10 | }; 11 | 12 | const AmbientBackdrop = ({ getBackgroundStyleAttribute, imageId }: Props) => { 13 | const backdropRef = useRef(null); 14 | const { imageStore } = useStore(); 15 | 16 | if (imageId) { 17 | imageStore.loadColor(imageId); 18 | } 19 | 20 | const currentColor = get(imageStore.colors, imageId); 21 | let background = 'black'; 22 | 23 | if (currentColor) { 24 | background = getBackgroundStyleAttribute(currentColor); 25 | } else if (backdropRef.current) { 26 | background = backdropRef.current.style.background; 27 | } 28 | 29 | return ( 30 |
35 | ); 36 | }; 37 | 38 | export default observer(AmbientBackdrop); 39 | -------------------------------------------------------------------------------- /component/Queue/QueueListItem/QueueListItem.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | $track-info-width: 528px; 4 | $track-info-margin-left: 32px; 5 | 6 | .queueListItem { 7 | @include transition-background-opacity; 8 | 9 | width: 100%; 10 | height: 100%; 11 | display: flex; 12 | align-items: center; 13 | } 14 | 15 | .imageContainer { 16 | margin-left: 40px; 17 | } 18 | 19 | .image { 20 | height: 96px; 21 | width: 96px; 22 | } 23 | 24 | .trackInfo { 25 | width: $track-info-width; 26 | margin-left: $track-info-margin-left; 27 | } 28 | 29 | .title { 30 | overflow: hidden; 31 | text-overflow: ellipsis; 32 | white-space: nowrap; 33 | margin-bottom: 8px; 34 | opacity: 0.7; 35 | 36 | &.currentlyPlaying { 37 | color: $green-light; 38 | } 39 | } 40 | 41 | .subtitle { 42 | overflow: hidden; 43 | text-overflow: ellipsis; 44 | white-space: nowrap; 45 | } 46 | 47 | .selected { 48 | background: $opacity-white-10; 49 | 50 | .title { 51 | opacity: 1; 52 | } 53 | 54 | &.pressed { 55 | opacity: 0.5; 56 | } 57 | } 58 | 59 | .pressed { 60 | background: $opacity-white-10; 61 | opacity: 0.5; 62 | } 63 | -------------------------------------------------------------------------------- /@spotify-internal/encore-foundation/index.js: -------------------------------------------------------------------------------- 1 | export * from "./web/tokens.es6.js"; 2 | export * from "./themes/themes.es6.js"; 3 | export { ballad as desktopBallad, balladBold as desktopBalladBold, viola as desktopViola, violaBold as desktopViolaBold, mesto as desktopMesto, mestoBold as desktopMestoBold, bass as desktopBass, forte as desktopForte, brio as desktopBrio, metronome as desktopMetronome, minuet as desktopMinuet, minuetBold as desktopMinuetBold, finale as desktopFinale, finaleBold as desktopFinaleBold, altoBrio as desktopAltoBrio, alto as desktopAlto, canon as desktopCanon, celloCanon as desktopCelloCanon, cello as desktopCello } from "./desktop/tokens.es6.js"; 4 | export { ballad as mobileBallad, balladBold as mobileBalladBold, viola as mobileViola, violaBold as mobileViolaBold, mesto as mobileMesto, mestoBold as mobileMestoBold, bass as mobileBass, forte as mobileForte, brio as mobileBrio, finale as mobileFinale, finaleBold as mobileFinaleBold, minuet as mobileMinuet, minuetBold as mobileMinuetBold, metronome as mobileMetronome, altoBrio as mobileAltoBrio, alto as mobileAlto, canon as mobileCanon, celloCanon as mobileCelloCanon, cello as mobileCello } from "./mobile/tokens.es6.js"; 5 | -------------------------------------------------------------------------------- /@spotify-internal/encore-foundation/desktop/index.js: -------------------------------------------------------------------------------- 1 | export * from "./web/tokens.es6.js"; 2 | export * from "./themes/themes.es6.js"; 3 | export { ballad as desktopBallad, balladBold as desktopBalladBold, viola as desktopViola, violaBold as desktopViolaBold, mesto as desktopMesto, mestoBold as desktopMestoBold, bass as desktopBass, forte as desktopForte, brio as desktopBrio, metronome as desktopMetronome, minuet as desktopMinuet, minuetBold as desktopMinuetBold, finale as desktopFinale, finaleBold as desktopFinaleBold, altoBrio as desktopAltoBrio, alto as desktopAlto, canon as desktopCanon, celloCanon as desktopCelloCanon, cello as desktopCello } from "./desktop/tokens.es6.js"; 4 | export { ballad as mobileBallad, balladBold as mobileBalladBold, viola as mobileViola, violaBold as mobileViolaBold, mesto as mobileMesto, mestoBold as mobileMestoBold, bass as mobileBass, forte as mobileForte, brio as mobileBrio, finale as mobileFinale, finaleBold as mobileFinaleBold, minuet as mobileMinuet, minuetBold as mobileMinuetBold, metronome as mobileMetronome, altoBrio as mobileAltoBrio, alto as mobileAlto, canon as mobileCanon, celloCanon as mobileCelloCanon, cello as mobileCello } from "./mobile/tokens.es6.js"; 5 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/AppendEllipsis/AppendEllipsis.module.scss: -------------------------------------------------------------------------------- 1 | .dot { 2 | margin-left: 4px; 3 | animation-duration: 4s; 4 | animation-iteration-count: infinite; 5 | animation-timing-function: steps(1); 6 | } 7 | 8 | .dot1 { 9 | animation-name: dot1; 10 | } 11 | 12 | .dot2 { 13 | animation-name: dot2; 14 | } 15 | 16 | .dot3 { 17 | animation-name: dot3; 18 | } 19 | 20 | @keyframes dot1 { 21 | 0% { 22 | opacity: 0; 23 | } 24 | 25 | 25% { 26 | opacity: 1; 27 | } 28 | 29 | 50% { 30 | opacity: 1; 31 | } 32 | 33 | 75% { 34 | opacity: 1; 35 | } 36 | 37 | 100% { 38 | opacity: 0; 39 | } 40 | } 41 | 42 | @keyframes dot2 { 43 | 0% { 44 | opacity: 0; 45 | } 46 | 47 | 25% { 48 | opacity: 0; 49 | } 50 | 51 | 50% { 52 | opacity: 1; 53 | } 54 | 55 | 75% { 56 | opacity: 1; 57 | } 58 | 59 | 100% { 60 | opacity: 0; 61 | } 62 | } 63 | 64 | @keyframes dot3 { 65 | 0% { 66 | opacity: 0; 67 | } 68 | 69 | 25% { 70 | opacity: 0; 71 | } 72 | 73 | 50% { 74 | opacity: 0; 75 | } 76 | 77 | 75% { 78 | opacity: 1; 79 | } 80 | 81 | 100% { 82 | opacity: 0; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /component/Npv/PlayingInfo/PlayingInfoTitles/PlayingInfoTitles.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | $info-container-width: 432px; 4 | 5 | .transitionContainer { 6 | position: absolute; 7 | } 8 | 9 | .texts { 10 | margin-top: 16px; 11 | } 12 | 13 | .songTitle { 14 | color: $white; 15 | font-weight: bold; 16 | width: $info-container-width; 17 | text-overflow: ellipsis; 18 | overflow: hidden; 19 | display: -webkit-box; 20 | word-break: break-word; 21 | -webkit-line-clamp: 3; 22 | -webkit-box-orient: vertical; 23 | padding-bottom: 5px; 24 | } 25 | 26 | .songTitleBig { 27 | @include text-style(72px, 72px, -0.03em); 28 | } 29 | 30 | .songTitleMiddle { 31 | @include text-style(52px, 56px, -0.03em); 32 | } 33 | 34 | .songTitleSmall { 35 | @include text-style(44px, 47px, -0.02em); 36 | 37 | max-height: 144px; 38 | } 39 | 40 | .artistTitle { 41 | @include text-style(36px, 40px, -0.01em); 42 | 43 | max-height: 80px; 44 | margin-top: 16px; 45 | width: $info-container-width; 46 | text-overflow: ellipsis; 47 | overflow: hidden; 48 | white-space: nowrap; 49 | position: relative; 50 | color: $white; 51 | padding-bottom: 3px; 52 | } 53 | -------------------------------------------------------------------------------- /component/Settings/Submenu/SubmenuItem.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .item { 4 | @include transition-background-opacity; 5 | 6 | display: flex; 7 | justify-content: space-between; 8 | align-items: center; 9 | padding: 0 32px; 10 | height: 96px; 11 | color: $white; 12 | opacity: 0.7; 13 | 14 | @include transition-margin-right-opacity; 15 | } 16 | 17 | .disabled { 18 | opacity: 0.3; 19 | } 20 | 21 | .pressed { 22 | background: $opacity-white-10; 23 | opacity: 0.5; 24 | } 25 | 26 | .onOffToggle { 27 | text-align: center; 28 | margin-right: 13px; 29 | color: $white; 30 | opacity: 0.5; 31 | @include transition-transform; 32 | } 33 | 34 | .keyValueValue { 35 | @include transition-transform; 36 | 37 | color: $white; 38 | } 39 | 40 | .movedForDial { 41 | transform: translateX(-56px); 42 | } 43 | 44 | .green { 45 | color: $green-light; 46 | opacity: 0.7; 47 | } 48 | 49 | .white70 { 50 | color: $white-70; 51 | } 52 | 53 | .active:not(.disabled) { 54 | background: $opacity-white-10; 55 | opacity: 1; 56 | 57 | &.pressed { 58 | opacity: 0.5; 59 | } 60 | 61 | .onOffToggle { 62 | opacity: 1; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /component/Shelf/VoiceMutedbanner/VoiceMutedBanner.tsx: -------------------------------------------------------------------------------- 1 | import { observer } from 'mobx-react-lite'; 2 | import { useStore } from 'context/store'; 3 | import Banner from 'component/CarthingUIComponents/Banner/Banner'; 4 | import BannerButton from 'component/CarthingUIComponents/Banner/BannerButton'; 5 | import { IconMicOff32 } from 'component/CarthingUIComponents'; 6 | import { useEffect } from 'react'; 7 | import { runInAction } from 'mobx'; 8 | 9 | const VoiceMutedBanner = () => { 10 | const uiState = useStore().shelfStore.shelfController.voiceMuteBannerUiState; 11 | 12 | useEffect(() => { 13 | runInAction(() => { 14 | if (uiState.shouldShowAlert) { 15 | uiState.logImpression(); 16 | } 17 | }); 18 | }); 19 | 20 | const infoText = 'Turn on your mic to make voice requests.'; 21 | 22 | const icon = ; 23 | 24 | return ( 25 | 26 | uiState.handleClickUnmute()} 30 | /> 31 | 32 | ); 33 | }; 34 | 35 | export default observer(VoiceMutedBanner); 36 | -------------------------------------------------------------------------------- /component/LazyImage/LazyImage.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .imageCenter { 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | } 8 | 9 | .outerBorder { 10 | @include transition-border-color; 11 | 12 | border-color: transparent; 13 | 14 | &.outerBorderActive { 15 | box-sizing: content-box; 16 | border: 4px solid; 17 | padding: 6px; 18 | } 19 | } 20 | 21 | .innerBorder { 22 | &.episode { 23 | border-radius: 16px; 24 | border: 20px solid; 25 | } 26 | 27 | &.track { 28 | border: 20px solid; 29 | } 30 | } 31 | 32 | .image { 33 | max-height: 100%; 34 | max-width: 100%; 35 | object-fit: cover; 36 | transform-origin: top left; 37 | 38 | &.shaded { 39 | filter: brightness(30%); 40 | } 41 | &.track { 42 | border-radius: 16px; 43 | } 44 | } 45 | 46 | .radioStation { 47 | position: relative; 48 | height: 100%; 49 | width: 100%; 50 | 51 | .radioStationBg { 52 | position: absolute; 53 | top: 0; 54 | left: 0; 55 | } 56 | 57 | .imageRadio { 58 | position: absolute; 59 | margin: auto; 60 | top: 0; 61 | left: 0; 62 | bottom: 0; 63 | right: 0; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /component/Npv/PlayingInfo/PlayingInfo.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | $info-container-width: 432px; 4 | $info-container-height: 248px; 5 | 6 | .playingInfo { 7 | height: 100%; 8 | width: 100%; 9 | padding: 40px 40px 48px; 10 | display: flex; 11 | } 12 | 13 | .animationEnterLeft { 14 | opacity: 0; 15 | transform: translateX(-24px); 16 | } 17 | 18 | .animationEnterRight { 19 | opacity: 0; 20 | transform: translateX(24px); 21 | } 22 | 23 | .animationEnterActive { 24 | opacity: 1; 25 | transform: translateX(0); 26 | 27 | @include transition-opacity-transform; 28 | } 29 | 30 | .animationExit { 31 | opacity: 1; 32 | transform: translateX(0); 33 | } 34 | 35 | .animationExitLeft { 36 | opacity: 0; 37 | transform: translateX(-24px); 38 | 39 | @include transition-opacity-transform; 40 | } 41 | 42 | .animationExitRight { 43 | opacity: 0; 44 | transform: translateX(24px); 45 | 46 | @include transition-opacity-transform; 47 | } 48 | 49 | .info { 50 | margin-left: 34px; 51 | width: $info-container-width; 52 | height: $info-container-height; 53 | } 54 | 55 | .playingInfoHeader { 56 | display: flex; 57 | justify-content: space-between; 58 | height: 32px; 59 | } 60 | -------------------------------------------------------------------------------- /component/PhoneCall/PhoneCallTimer.tsx: -------------------------------------------------------------------------------- 1 | import { observer } from 'mobx-react-lite'; 2 | import styles from 'component/PhoneCall/PhoneCallTimer.module.scss'; 3 | import { useStore } from 'context/store'; 4 | import { CSSTransition } from 'react-transition-group'; 5 | import CountUpTimer from 'component/CountUpTimer/CountUpTimer'; 6 | import Type from 'component/CarthingUIComponents/Type/Type'; 7 | import { transitionDurationMs } from 'style/Variables'; 8 | 9 | const transitionStyles = { 10 | enter: styles.enter, 11 | enterActive: styles.enterActive, 12 | exit: styles.exit, 13 | exitActive: styles.exitActive, 14 | }; 15 | 16 | const PhoneCallTimer = () => { 17 | const uiState = useStore().phoneCallController.phoneCallUiState; 18 | 19 | return ( 20 | 26 | 27 |
28 | 29 |
30 |
31 |
32 | ); 33 | }; 34 | 35 | export default observer(PhoneCallTimer); 36 | -------------------------------------------------------------------------------- /component/OtaUpdating/OtaUpdating.tsx: -------------------------------------------------------------------------------- 1 | import { observer } from 'mobx-react-lite'; 2 | 3 | import styles from './OtaUpdating.module.scss'; 4 | import { useStore } from 'context/store'; 5 | import AppendEllipsis from 'component/CarthingUIComponents/AppendEllipsis/AppendEllipsis'; 6 | 7 | const OtaUpdating = () => { 8 | const { otaStore } = useStore(); 9 | return ( 10 |
11 | {otaStore.error ? ( 12 | <> 13 |
更新失败
14 |
15 | 若要重启设备并重试,请拔下设备电源,稍等一段时间后插入即可。 16 |
17 | 18 | ) : ( 19 | <> 20 |
21 | 升级中 22 |
23 |
24 | 请保证设备电源供应稳定,不要断开电源。 25 |
26 | {otaStore.transferring && ( 27 |
{`${otaStore.transferPercent}%`}
30 | )} 31 | 32 | )} 33 |
34 | ); 35 | }; 36 | 37 | export default observer(OtaUpdating); 38 | -------------------------------------------------------------------------------- /component/Npv/PlayingInfo/StatusIcons.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | IconMute32, 3 | IconRepeat32, 4 | IconRepeatOne32, 5 | IconWind32, 6 | } from 'component/CarthingUIComponents'; 7 | import { useStore } from 'context/store'; 8 | import { observer } from 'mobx-react-lite'; 9 | import styles from './StatusIcons.module.scss'; 10 | 11 | const StatusIcons = () => { 12 | const uiState = useStore().npvStore.playingInfoUiState; 13 | 14 | return ( 15 |
16 | {uiState.showWindLevelIcon && ( 17 |
18 | 19 |
20 | )} 21 | {uiState.isPlayingSpotify && uiState.onRepeat && ( 22 |
23 | 24 |
25 | )} 26 | {uiState.isPlayingSpotify && uiState.onRepeatOnce && ( 27 |
28 | 29 |
30 | )} 31 | {uiState.isMicMuted && ( 32 |
33 | 34 |
35 | )} 36 |
37 | ); 38 | }; 39 | 40 | export default observer(StatusIcons); 41 | -------------------------------------------------------------------------------- /component/Modals/Modal.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .title { 4 | @include text-style(52px, 56px, -0.03em); 5 | 6 | margin-top: 24px; 7 | margin-bottom: 32px; 8 | font-weight: bold; 9 | } 10 | 11 | .subtitle { 12 | margin-top: 32px; 13 | flex-direction: column; 14 | align-items: center; 15 | 16 | @include text-style(44px, 48px, -0.02em); 17 | 18 | font-weight: bold; 19 | 20 | p { 21 | overflow: hidden; 22 | text-overflow: ellipsis; 23 | white-space: nowrap; 24 | } 25 | } 26 | 27 | .description { 28 | @include text-style(32px, 40px, -0.01em); 29 | 30 | margin-bottom: 40px; 31 | width: 540px; 32 | } 33 | 34 | .pairingCode { 35 | @include text-style(52px, 56px, -0.03em); 36 | 37 | color: $green-light; 38 | } 39 | 40 | .iconCheck { 41 | margin-top: 42px; 42 | margin-bottom: 42px; 43 | transform: scale(calc(104 / 16)); 44 | color: $green-light; 45 | } 46 | 47 | .dialog { 48 | padding: 0 100px; 49 | display: flex; 50 | flex-direction: column; 51 | align-items: center; 52 | text-align: center; 53 | justify-content: center; 54 | height: $device-height; 55 | background-color: $opacity-black-90; 56 | } 57 | 58 | .logoSpotify { 59 | color: $white; 60 | } 61 | -------------------------------------------------------------------------------- /component/Onboarding/DialTurnDots.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .outerCircle { 4 | width: 432px; 5 | height: 432px; 6 | background: $white; 7 | z-index: $onboarding-tactile-z-index; 8 | position: absolute; 9 | left: 688px; 10 | top: -61px; 11 | border-radius: 50%; 12 | } 13 | 14 | .innerCircle { 15 | width: 300px; 16 | height: 300px; 17 | background: $gray-15; 18 | z-index: $onboarding-tactile-z-index + 1; 19 | position: absolute; 20 | border: 1px solid black; 21 | border-radius: 50%; 22 | animation-name: spin; 23 | animation-duration: 5000ms; 24 | animation-iteration-count: infinite; 25 | animation-timing-function: linear; 26 | top: 65px; 27 | right: 65px; 28 | } 29 | 30 | .dot1 { 31 | position: absolute; 32 | top: 25px; 33 | left: 0; 34 | width: 20px; 35 | height: 20px; 36 | background: $green; 37 | border-radius: 50%; 38 | } 39 | 40 | .dot2 { 41 | position: absolute; 42 | top: 290px; 43 | left: 240px; 44 | width: 20px; 45 | height: 20px; 46 | background: $green; 47 | border-radius: 50%; 48 | } 49 | 50 | @keyframes spin { 51 | from { 52 | transform: rotate(0deg); 53 | } 54 | 55 | to { 56 | transform: rotate(360deg); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /component/Npv/ControlButtons/PlayPause.tsx: -------------------------------------------------------------------------------- 1 | import { IconPause48, IconPlay48 } from 'component/CarthingUIComponents'; 2 | import ControlButton from 'component/Npv/ControlButtons/ControlButton'; 3 | import { NpvIcon } from 'component/Npv/ControlButtons/Controls'; 4 | import { useStore } from 'context/store'; 5 | import { observer } from 'mobx-react-lite'; 6 | 7 | const PlayPause = () => { 8 | const uiState = useStore().npvStore.controlButtonsUiState; 9 | const { playerStore } = useStore(); 10 | return ( 11 | <> 12 | {uiState.isPlaying ? ( 13 | 18 |
19 | 20 |
21 |
22 | ) : ( 23 | 28 |
29 | 30 |
31 |
32 | )} 33 | 34 | ); 35 | }; 36 | 37 | export default observer(PlayPause); 38 | -------------------------------------------------------------------------------- /@spotify-internal/uri/src/_internal/helpers.ts: -------------------------------------------------------------------------------- 1 | import {URIFormat} from '../enums/uri_format'; 2 | 3 | /** 4 | * Encodes a component according to a URIformat. 5 | * 6 | * @param component - A component string. 7 | * @param format - A URIformat. 8 | * @return An encoded component string. 9 | */ 10 | export function encodeComponent( 11 | component?: string, 12 | format?: URIFormat 13 | ): string { 14 | if (!component) { 15 | return ''; 16 | } 17 | let encodedComponent = encodeURIComponent(component); 18 | if (format === URIFormat.URI) { 19 | encodedComponent = encodedComponent.replace(/%20/g, '+'); 20 | } 21 | 22 | // encode characters that are not encoded by default by encodeURIComponent 23 | // but that the Spotify URI spec encodes: !'*() 24 | encodedComponent = encodedComponent.replace(/[!'()]/g, escape); 25 | encodedComponent = encodedComponent.replace(/\*/g, '%2A'); 26 | 27 | return encodedComponent; 28 | } 29 | 30 | export function decodeComponent( 31 | component?: string, 32 | format?: URIFormat 33 | ): string { 34 | if (!component) { 35 | return ''; 36 | } 37 | 38 | const part = 39 | format === URIFormat.URI ? component.replace(/\+/g, '%20') : component; 40 | return decodeURIComponent(part); 41 | } 42 | -------------------------------------------------------------------------------- /component/Npv/Scrubbing/ScrubbingBar.tsx: -------------------------------------------------------------------------------- 1 | import { useStore } from 'context/store'; 2 | import { observer } from 'mobx-react-lite'; 3 | import styles from './ScrubbingBar.module.scss'; 4 | 5 | /** 30 行开始为定义小图标的代码段 */ 6 | /** marginTop 代表了小图标上浮的距离,可以根据小图标主体信息的位置来确定 */ 7 | /** img 标签的 height 和 width 两个属性代表了图标的大小,不宜设置过大。 */ 8 | /** 如果不需要显示该图标,请删除第 30 - 37 行 */ 9 | 10 | const ScrubbingBar = () => { 11 | const uiState = useStore().npvStore.scrubbingUiState; 12 | const { colorChannels } = uiState; 13 | 14 | return ( 15 |
22 | 23 |
29 | 30 |
36 | 37 |
38 | 39 |
40 | ); 41 | }; 42 | 43 | export default observer(ScrubbingBar); 44 | -------------------------------------------------------------------------------- /component/Presets/PresetIndicator/PresetNumberIndicator.tsx: -------------------------------------------------------------------------------- 1 | import { observer } from 'mobx-react-lite'; 2 | import styles from 'component/Presets/PresetIndicator/PresetNumberIndicator.module.scss'; 3 | import Type from 'component/CarthingUIComponents/Type/Type'; 4 | import classNames from 'classnames'; 5 | import { useStore } from 'context/store'; 6 | import { PresetNumber } from 'store/PresetsDataStore'; 7 | 8 | type Props = { 9 | presetNumber: PresetNumber; 10 | }; 11 | const PresetNumberIndicator = ({ presetNumber }: Props) => { 12 | const uiState = useStore().presetsController.presetsUiState; 13 | 14 | const isFocused = uiState.selectedPresetNumber === presetNumber; 15 | return ( 16 |
23 |
24 | 25 |
26 | 27 | {presetNumber} 28 | 29 |
30 |
31 | ); 32 | }; 33 | 34 | export default observer(PresetNumberIndicator); 35 | -------------------------------------------------------------------------------- /component/Settings/DisplayAndBrightness/DisplayAndBrightness.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | .header { 4 | position: sticky; 5 | background-color: $black; 6 | z-index: $overlay-z-index + 20; 7 | height: 128px; 8 | width: 100%; 9 | display: flex; 10 | align-items: center; 11 | 12 | span { 13 | margin-left: 40px; 14 | color: $white; 15 | } 16 | } 17 | 18 | .container { 19 | height: 352px; 20 | overflow: scroll; 21 | background: $black; 22 | 23 | .notification { 24 | height: 96px; 25 | margin-bottom: 40px; 26 | padding-left: 40px; 27 | padding-right: 72px; 28 | display: flex; 29 | justify-content: space-between; 30 | align-items: center; 31 | font-weight: 700; 32 | @include text-style(36px, 48px, -0.02em); 33 | @include transition-background-border; 34 | 35 | background: $white-10; 36 | 37 | &.pressed { 38 | color: $white-70; 39 | background: rgb(255 255 255 / 5%); 40 | border: none; 41 | } 42 | } 43 | 44 | .text { 45 | color: $gray-80; 46 | padding-left: 40px; 47 | padding-right: 40px; 48 | } 49 | } 50 | 51 | ::-webkit-scrollbar { 52 | width: 0; 53 | height: 0; 54 | background: transparent; /* make scrollbar transparent */ 55 | } 56 | -------------------------------------------------------------------------------- /component/Listening/Listening.module.scss: -------------------------------------------------------------------------------- 1 | @import 'style/variables.module'; 2 | 3 | $listening-wrapper-padding: 40px; 4 | 5 | .listeningWrapper { 6 | height: $device-height; 7 | width: $device-width; 8 | background: $black; 9 | padding: $listening-wrapper-padding; 10 | } 11 | 12 | .currentlyListening { 13 | background: radial-gradient( 14 | 100% 100% at 0% 100%, 15 | #000 27.85%, 16 | rgb(0 0 0 / 60%) 100% 17 | ); 18 | } 19 | 20 | .centered { 21 | position: absolute; 22 | left: 50%; 23 | top: 50%; 24 | transform: translateX(-50%) translateY(-50%); 25 | } 26 | 27 | .voiceConfirmation { 28 | display: flex; 29 | flex-direction: column; 30 | align-items: center; 31 | } 32 | 33 | .voiceConfirmationText { 34 | margin-top: 40px; 35 | } 36 | 37 | .textConfirmation { 38 | overflow: hidden; 39 | -webkit-line-clamp: 3; 40 | -webkit-box-orient: vertical; 41 | display: -webkit-box; 42 | } 43 | 44 | .jellyfish { 45 | position: absolute; 46 | bottom: $listening-wrapper-padding; 47 | left: $listening-wrapper-padding; 48 | height: 156px; 49 | } 50 | 51 | .fadeIn { 52 | animation: fade-in 1.5s forwards; 53 | } 54 | 55 | @keyframes fade-in { 56 | 0% { 57 | opacity: 0; 58 | } 59 | 60 | 100% { 61 | opacity: 1; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /component/Npv/Volume/Volume.tsx: -------------------------------------------------------------------------------- 1 | import { IconVolume48, IconVolumeOff48 } from 'component/CarthingUIComponents'; 2 | import Type from 'component/CarthingUIComponents/Type/Type'; 3 | import { useStore } from 'context/store'; 4 | import { observer } from 'mobx-react-lite'; 5 | import styles from './Volume.module.scss'; 6 | import VolumeBar from './VolumeBar'; 7 | 8 | const Volume = () => { 9 | const uiState = useStore().npvStore.volumeUiState; 10 | 11 | return ( 12 |
13 | {uiState.carMode ? ( 14 | <> 15 | 16 | 在 {uiState.carMode} 情况下手机音量不可用 17 | 18 | 19 | ) : ( 20 | <> 21 | 22 |
25 |
28 | {uiState.isVolumeAbove0 ? : } 29 |
30 | 31 | 手机音量 32 | 33 |
34 | 35 | )} 36 |
37 | ); 38 | }; 39 | 40 | export default observer(Volume); 41 | -------------------------------------------------------------------------------- /component/CarthingUIComponents/Type/Type.tsx: -------------------------------------------------------------------------------- 1 | import './Type.scss'; 2 | import classNames from 'classnames'; 3 | import React, { CSSProperties } from 'react'; 4 | 5 | export type TypeName = 6 | | 'bassBold' 7 | | 'bassBook' 8 | | 'forteBold' 9 | | 'forteBook' 10 | | 'brioBold' 11 | | 'brioBook' 12 | | 'altoBold' 13 | | 'altoBook' 14 | | 'canonBold' 15 | | 'canonBook' 16 | | 'celloBold' 17 | | 'celloBook' 18 | | 'balladBold' 19 | | 'balladBook' 20 | | 'mestroBold' 21 | | 'mestroBook' 22 | | 'minuet'; 23 | 24 | type Props = { 25 | children: React.ReactNode; 26 | name: TypeName; 27 | textColor?: string; 28 | dataTestId?: string; 29 | className?: string; 30 | onClick?: (e?: any) => void; 31 | style?: CSSProperties; 32 | }; 33 | 34 | const Type = React.forwardRef( 35 | ( 36 | { children, name, textColor, className, dataTestId, onClick, style }: Props, 37 | ref, 38 | ) => { 39 | return ( 40 |
47 | {children} 48 |
49 | ); 50 | }, 51 | ); 52 | 53 | export default Type; 54 | -------------------------------------------------------------------------------- /component/Npv/ControlButtons/ControlButton.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import pointerListenersMaker from 'helpers/PointerListeners'; 3 | import { useState } from 'react'; 4 | import * as React from 'react'; 5 | import styles from './Controls.module.scss'; 6 | 7 | type Props = { 8 | id: string; 9 | children?: React.ReactNode; 10 | onClick?: (e?) => void; 11 | fullSize?: boolean; 12 | isDisabled?: boolean; 13 | }; 14 | 15 | const ControlButton = ({ 16 | id, 17 | onClick, 18 | children, 19 | fullSize = false, 20 | isDisabled = false, 21 | }: Props) => { 22 | const [touchDown, setTouchDown] = useState(false); 23 | 24 | const onClickProps = { 25 | onClick, 26 | ...pointerListenersMaker(setTouchDown), 27 | }; 28 | 29 | return ( 30 |
31 |
40 | {children} 41 |
42 |
43 | ); 44 | }; 45 | 46 | export default ControlButton; 47 | --------------------------------------------------------------------------------