) => {
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------
/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** 
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 |
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 |
14 | );
15 |
16 | export default IconPhoneAnswer;
17 |
--------------------------------------------------------------------------------
/component/CarthingUIComponents/Icons/IconPhoneDecline.tsx:
--------------------------------------------------------------------------------
1 | const IconPhoneDecline = () => (
2 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------