Browse Source

update player functionality

master
jeffvli 3 years ago
parent
commit
0ec986865f
  1. 1
      package.json
  2. 33
      src/App.tsx
  3. 3
      src/components/layout/styled.tsx
  4. 62
      src/components/player/Player.tsx
  5. 323
      src/components/player/PlayerBar.tsx
  6. 41
      src/redux/playQueueSlice.ts
  7. 24
      yarn.lock

1
package.json

@ -252,6 +252,7 @@
"electron-redux": "^1.5.4",
"electron-settings": "^4.0.2",
"electron-updater": "^4.3.4",
"format-duration": "^1.4.0",
"history": "^5.0.0",
"lodash": "^4.17.21",
"md5": "^2.3.0",

33
src/App.tsx

@ -6,7 +6,6 @@ import PlaylistList from './components/playlist/PlaylistList';
import PlaylistView from './components/playlist/PlaylistView';
import Settings from './components/settings/Settings';
import NowPlayingView from './components/player/NowPlayingView';
import Player from './components/player/Player';
import Login from './components/settings/Login';
import StarredView from './components/starred/StarredView';
import Dashboard from './components/dashboard/Dashboard';
@ -19,23 +18,21 @@ const App = () => {
}
return (
<Player>
<Router>
<Layout footer={<PlayerBar />}>
<Switch>
<Route path="/library/artist/:id" component={NowPlayingView} />
<Route path="/library/album/:id" component={NowPlayingView} />
<Route path="/library" component={LibraryView} />
<Route path="/nowplaying" component={NowPlayingView} />
<Route path="/settings" component={Settings} />
<Route path="/playlist/:id" component={PlaylistView} />
<Route path="/playlists" component={PlaylistList} />
<Route path="/starred" component={StarredView} />
<Route path="/" component={Dashboard} />
</Switch>
</Layout>
</Router>
</Player>
<Router>
<Layout footer={<PlayerBar />}>
<Switch>
<Route path="/library/artist/:id" component={NowPlayingView} />
<Route path="/library/album/:id" component={NowPlayingView} />
<Route path="/library" component={LibraryView} />
<Route path="/nowplaying" component={NowPlayingView} />
<Route path="/settings" component={Settings} />
<Route path="/playlist/:id" component={PlaylistView} />
<Route path="/playlists" component={PlaylistList} />
<Route path="/starred" component={StarredView} />
<Route path="/" component={Dashboard} />
</Switch>
</Layout>
</Router>
);
};

3
src/components/layout/styled.tsx

@ -5,7 +5,6 @@ import { Container, Content, Footer, Header, Sidebar } from 'rsuite';
// Layout.tsx
export const RootContainer = styled(Container)`
height: 100vh;
padding-bottom: 10px;
`;
interface ContainerProps {
@ -29,7 +28,7 @@ export const MainContainer = styled(StyledContainer)`
`;
export const RootFooter = styled(Footer)`
height: 88px;
height: 98px;
`;
// Titlebar.tsx

62
src/components/player/Player.tsx

@ -1,4 +1,11 @@
import React, { useState, createContext, useRef, useEffect } from 'react';
import React, {
useState,
createContext,
useRef,
useEffect,
useImperativeHandle,
forwardRef,
} from 'react';
import ReactAudioPlayer from 'react-audio-player';
import { useAppDispatch, useAppSelector } from '../../redux/hooks';
import {
@ -6,19 +13,27 @@ import {
incrementPlayerIndex,
setCurrentPlayer,
setPlayerVolume,
setCurrentSeek,
} from '../../redux/playQueueSlice';
export const PlayerContext = createContext<any>({});
const Player = ({ children }: any) => {
const Player = ({ children }: any, ref: any) => {
const [incremented, setIncremented] = useState(false);
const player1Ref = useRef<any>();
const player2Ref = useRef<any>();
const dispatch = useAppDispatch();
const playQueue = useAppSelector((state) => state.playQueue);
useImperativeHandle(ref, () => ({
get player1() {
return player1Ref.current;
},
get player2() {
return player2Ref.current;
},
}));
useEffect(() => {
if (playQueue.status === 'PLAYING') {
if (playQueue.currentPlayer === 1) {
@ -42,11 +57,11 @@ const Player = ({ children }: any) => {
const handleListen = () => {
const fadeDuration = 10;
const currentTime = player1Ref.current?.audioEl.current?.currentTime || 0;
const currentSeek = player1Ref.current?.audioEl.current?.currentTime || 0;
const duration = player1Ref.current?.audioEl.current?.duration;
const fadeAtTime = duration - fadeDuration;
if (currentTime >= fadeAtTime) {
if (currentSeek >= fadeAtTime) {
if (player2Ref.current.audioEl.current) {
// Once fading starts, start playing player 2 and set current to 2
const player1Volume =
@ -72,17 +87,19 @@ const Player = ({ children }: any) => {
dispatch(setCurrentPlayer(2));
}
console.log('fading player1...');
} else {
dispatch(setCurrentSeek(currentSeek));
}
console.log(`player1: ${currentTime} / ${fadeAtTime}`);
console.log(`player1: ${currentSeek} / ${fadeAtTime}`);
};
const handleListen2 = () => {
const fadeDuration = 10;
const currentTime = player2Ref.current?.audioEl.current?.currentTime || 0;
const currentSeek = player2Ref.current?.audioEl.current?.currentTime || 0;
const duration = player2Ref.current?.audioEl.current?.duration;
const fadeAtTime = duration - fadeDuration;
if (currentTime >= fadeAtTime) {
if (currentSeek >= fadeAtTime) {
if (player1Ref.current.audioEl.current) {
// Once fading starts, start playing player 1 and set current to 1
const player1Volume =
@ -108,8 +125,11 @@ const Player = ({ children }: any) => {
dispatch(setCurrentPlayer(1));
}
console.log('fading player2...');
} else {
dispatch(setCurrentSeek(currentSeek));
}
console.log(`player2: ${currentTime} / ${fadeAtTime}`);
console.log(`player2: ${currentSeek} / ${fadeAtTime}`);
};
const handleOnEnded1 = () => {
@ -126,22 +146,6 @@ const Player = ({ children }: any) => {
setIncremented(false);
};
/* const handleOnLoadStart = () => {
dispatch(setIsLoading());
};
const handleOnLoadedData = () => {
dispatch(setIsLoaded());
};
const handleOnClickNext = () => {
dispatch(incrementCurrentIndex());
};
const handleOnClickPrevious = () => {
dispatch(decrementCurrentIndex());
}; */
return (
<PlayerContext.Provider
value={{
@ -150,7 +154,6 @@ const Player = ({ children }: any) => {
}}
>
<ReactAudioPlayer
style={{ position: 'absolute' }}
ref={player1Ref}
src={playQueue.entry[playQueue.player1.index]?.streamUrl}
listenInterval={500}
@ -158,11 +161,9 @@ const Player = ({ children }: any) => {
onListen={handleListen}
onEnded={handleOnEnded1}
volume={playQueue.player1.volume}
controls
autoPlay={playQueue.player1.index === playQueue.currentIndex}
/>
<ReactAudioPlayer
style={{ position: 'absolute' }}
ref={player2Ref}
src={playQueue.entry[playQueue.player2.index]?.streamUrl}
listenInterval={500}
@ -170,7 +171,6 @@ const Player = ({ children }: any) => {
onListen={handleListen2}
onEnded={handleOnEnded2}
volume={playQueue.player2.volume}
controls
autoPlay={playQueue.player2.index === playQueue.currentIndex}
/>
{children}
@ -178,4 +178,4 @@ const Player = ({ children }: any) => {
);
};
export default Player;
export default forwardRef(Player);

323
src/components/player/PlayerBar.tsx

@ -1,132 +1,229 @@
import React, { useContext, useEffect, useRef } from 'react';
import ReactAudioPlayer from 'react-audio-player';
import { Button } from 'rsuite';
import { useAppDispatch, useAppSelector } from '../../redux/hooks';
import React, { useEffect, useState, useRef } from 'react';
import { FlexboxGrid, Icon, Slider, Button } from 'rsuite';
import format from 'format-duration';
import styled from 'styled-components';
import {
incrementCurrentIndex,
incrementPlayerIndex,
decrementCurrentIndex,
setVolume,
setPlayerVolume,
setStatus,
} from '../../redux/playQueueSlice';
import { PlayerContext } from './Player';
import { useAppDispatch, useAppSelector } from '../../redux/hooks';
import Player from './Player';
import 'react-rangeslider/lib/index.css';
const PlayerBar = () => {
const player1Ref = useRef<any>();
const player2Ref = useRef<any>();
const {
player1Volume,
player2Volume,
setPlayer1Volume,
setPlayer2Volume,
incremented,
setIncremented,
globalVolume,
currentPlayer,
setCurrentPlayer,
} = useContext(PlayerContext);
const PlayerContainer = styled.div`
background: #000000;
height: 100%;
border-top: 1px solid #48545c;
`;
const dispatch = useAppDispatch();
const PlayerColumn = styled.div<{
left?: boolean;
center?: boolean;
right?: boolean;
height: string;
}>`
height: ${(props) => props.height};
display: flex;
align-items: center;
justify-content: ${(props) =>
props.left
? 'flex-start'
: props.center
? 'center'
: props.right
? 'flex-end'
: 'center'};
`;
const PlayerControlIcon = styled(Icon)`
color: #b3b3b3;
padding: 0 15px 0 15px;
&:hover {
color: #fff;
}
`;
const PlayerBar = () => {
const playQueue = useAppSelector((state) => state.playQueue);
const dispatch = useAppDispatch();
const [seek, setSeek] = useState(0);
const [isDragging, setIsDragging] = useState(false);
const [manualSeek, setManualSeek] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const playersRef = useRef<any>();
useEffect(() => {
setSeek(playQueue.currentSeek);
}, [playQueue.currentSeek]);
const handleListen = () => {
const fadeDuration = 10;
const currentTime = player1Ref.current?.audioEl.current?.currentTime || 0;
const duration = player1Ref.current?.audioEl.current?.duration;
const fadeAtTime = duration - fadeDuration;
if (currentTime >= fadeAtTime) {
if (player2Ref.current.audioEl.current) {
// Once fading starts, start playing player 2 and set current to 2
setPlayer1Volume(
player1Volume - globalVolume / (fadeDuration * 2) <= 0
? 0
: player1Volume - globalVolume / (fadeDuration * 2)
);
setPlayer2Volume(
player2Volume + globalVolume / (fadeDuration * 1.5) >= globalVolume
? globalVolume
: player2Volume + globalVolume / (fadeDuration * 1.5)
);
player2Ref.current.audioEl.current.play();
if (!incremented) {
dispatch(incrementCurrentIndex('none'));
setIncremented(true);
}
setCurrentPlayer(2);
useEffect(() => {
if (isDragging) {
if (playQueue.currentPlayer === 1) {
playersRef.current.player1.audioEl.current.currentTime = manualSeek;
} else {
playersRef.current.player2.audioEl.current.currentTime = manualSeek;
}
console.log('fading player1...');
// Wait for the seek to catch up, otherwise the bar will bounce back and forth
setTimeout(() => {
setIsDragging(false);
}, 1500);
}
console.log(`player1: ${currentTime} / ${fadeAtTime}`);
}, [isDragging, manualSeek, playQueue.currentPlayer]);
/* const handleOnLoadStart = () => {
dispatch(setIsLoading());
};
const handleListen2 = () => {
const fadeDuration = 10;
const currentTime = player2Ref.current?.audioEl.current?.currentTime || 0;
const duration = player2Ref.current?.audioEl.current?.duration;
const fadeAtTime = duration - fadeDuration;
if (currentTime >= fadeAtTime) {
if (player1Ref.current.audioEl.current) {
// Once fading starts, start playing player 1 and set current to 1
setPlayer1Volume(
player1Volume + globalVolume / (fadeDuration * 1.5) >= globalVolume
? globalVolume
: player1Volume + globalVolume / (fadeDuration * 1.5)
);
setPlayer2Volume(
player2Volume - globalVolume / (fadeDuration * 2) <= 0
? 0
: player2Volume - globalVolume / (fadeDuration * 2)
);
player1Ref.current.audioEl.current.play();
if (!incremented) {
dispatch(incrementCurrentIndex('none'));
setIncremented(true);
}
setCurrentPlayer(1);
}
console.log('fading player2...');
}
console.log(`player2: ${currentTime} / ${fadeAtTime}`);
const handleOnLoadedData = () => {
dispatch(setIsLoaded());
}; */
const handleClickNext = () => {
dispatch(incrementCurrentIndex('usingHotkey'));
};
const handleClickPrevious = () => {
dispatch(decrementCurrentIndex('usingHotkey'));
};
const handleOnEnded1 = () => {
dispatch(incrementPlayerIndex(1));
setPlayer1Volume(0);
setPlayer2Volume(globalVolume);
setIncremented(false);
const handleClickPlayPause = () => {
dispatch(setStatus(playQueue.status === 'PLAYING' ? 'PAUSED' : 'PLAYING'));
};
const handleOnEnded2 = () => {
dispatch(incrementPlayerIndex(2));
setPlayer1Volume(globalVolume);
setPlayer2Volume(0);
setIncremented(false);
const handleVolumeSlider = (e: number) => {
const vol = Number((e / 100).toFixed(2));
dispatch(setVolume(vol));
dispatch(setPlayerVolume({ player: playQueue.currentPlayer, volume: vol }));
};
const handleSeekSlider = (e: number) => {
setIsDragging(true);
setManualSeek(e);
console.log(e);
};
const handleOnWaiting = () => {
/* console.log(
(playersRef.current?.player1.audioEl.current.onwaiting = () => {
console.log('Waiting');
})
); */
};
return (
<>
<ReactAudioPlayer
ref={player1Ref}
src={playQueue.entry[playQueue.player1Index]?.streamUrl}
listenInterval={500}
preload="auto"
onListen={handleListen}
onEnded={handleOnEnded1}
controls
volume={player1Volume}
autoPlay={playQueue.player1Index === playQueue.currentIndex}
/>
<ReactAudioPlayer
ref={player2Ref}
src={playQueue.entry[playQueue.player2Index]?.streamUrl}
listenInterval={500}
preload="auto"
onListen={handleListen2}
onEnded={handleOnEnded2}
controls
volume={player2Volume}
autoPlay={playQueue.player2Index === playQueue.currentIndex}
/>
<Button onClick={() => console.log(playQueue.entry)}>Length</Button>
<PlayerContainer>
<Player ref={playersRef} isDragging={isDragging} />
<Button onClick={handleOnWaiting} />
<FlexboxGrid align="middle" style={{ height: '100%' }}>
<FlexboxGrid.Item
colspan={6}
style={{ textAlign: 'left', paddingLeft: '25px' }}
>
<PlayerColumn left height="50px">
<div>Is seeking: {isDragging ? 'true' : 'false'}</div>
</PlayerColumn>
</FlexboxGrid.Item>
<FlexboxGrid.Item
colspan={12}
style={{ textAlign: 'center', verticalAlign: 'middle' }}
>
<PlayerColumn center height="45px">
<PlayerControlIcon icon="random" size="lg" />
<PlayerControlIcon
icon="step-backward"
size="lg"
onClick={handleClickPrevious}
/>
<PlayerControlIcon
icon={
playQueue.status === 'PLAYING' ? 'pause-circle' : 'play-circle'
}
size="3x"
onClick={handleClickPlayPause}
/>
<PlayerControlIcon
icon="step-forward"
size="lg"
onClick={handleClickNext}
/>
<PlayerControlIcon
icon="repeat"
size="lg"
onClick={() => console.log('h')}
/>
</PlayerColumn>
<PlayerColumn center height="35px">
<FlexboxGrid
justify="center"
style={{
width: '100%',
display: 'flex',
alignItems: 'center',
height: '35px',
}}
>
<FlexboxGrid.Item
colspan={4}
style={{ textAlign: 'right', paddingRight: '10px' }}
>
{format((isDragging ? manualSeek : seek) * 1000)}
</FlexboxGrid.Item>
<FlexboxGrid.Item colspan={16}>
<Slider
progress
defaultValue={0}
value={isDragging ? manualSeek : seek}
tooltip={false}
max={
playQueue.entry[playQueue.currentIndex]?.duration -
10 * 1.3 || 0
}
onChange={handleSeekSlider}
style={{ width: '100%' }}
/>
</FlexboxGrid.Item>
<FlexboxGrid.Item
colspan={4}
style={{ textAlign: 'left', paddingLeft: '10px' }}
>
{format(
playQueue.entry[playQueue.currentIndex]?.duration * 1000 || 0
)}
</FlexboxGrid.Item>
</FlexboxGrid>
</PlayerColumn>
</FlexboxGrid.Item>
<FlexboxGrid.Item
colspan={6}
style={{ textAlign: 'right', paddingRight: '25px' }}
>
<PlayerColumn right height="45px">
<Icon
icon={
playQueue.volume > 0.7
? 'volume-up'
: playQueue.volume < 0.3
? 'volume-off'
: 'volume-down'
}
size="lg"
style={{ marginRight: '15px' }}
/>
<Slider
progress
value={Math.floor(playQueue.volume * 100)}
style={{ width: '80px' }}
onChange={handleVolumeSlider}
/>
</PlayerColumn>
</FlexboxGrid.Item>
</FlexboxGrid>
{/* <Button onClick={() => console.log(playQueue.entry)}>Length</Button>
<div>
{`Current index: ${playQueue.currentIndex} | `}
{`Player1 index: ${playQueue.player1Index} - ${
@ -136,8 +233,8 @@ const PlayerBar = () => {
playQueue.entry[playQueue.player2Index]?.title
} | `}
{`CurrentPlayer: ${playQueue.currentPlayer}`}
</div>
</>
</div> */}
</PlayerContainer>
);
};

41
src/redux/playQueueSlice.ts

@ -34,6 +34,7 @@ export interface PlayQueue {
currentIndex: number;
currentSongId: string;
currentPlayer: number;
currentSeek: number;
player1: {
index: number;
volume: number;
@ -53,6 +54,7 @@ const initialState: PlayQueue = {
currentIndex: 0,
currentSongId: '',
currentPlayer: 1,
currentSeek: 0,
player1: {
index: 0,
volume: 0.5,
@ -72,7 +74,17 @@ const playQueueSlice = createSlice({
initialState,
reducers: {
setStatus: (state, action: PayloadAction<string>) => {
state.status = action.payload;
if (state.entry.length >= 1) {
state.status = action.payload;
}
},
setVolume: (state, action: PayloadAction<number>) => {
state.volume = action.payload;
},
setCurrentSeek: (state, action: PayloadAction<number>) => {
state.currentSeek = action.payload;
},
setCurrentPlayer: (state, action: PayloadAction<number>) => {
@ -85,6 +97,7 @@ const playQueueSlice = createSlice({
incrementCurrentIndex: (state, action: PayloadAction<string>) => {
if (state.entry.length >= 1) {
state.currentSeek = 0;
if (state.currentIndex < state.entry.length - 1) {
state.currentIndex += 1;
if (action.payload === 'usingHotkey') {
@ -123,6 +136,7 @@ const playQueueSlice = createSlice({
(track) => track.id === action.payload.id
);
state.currentSeek = 0;
state.player1.index = findIndex;
state.player1.volume = state.volume;
state.player2.index = findIndex + 1;
@ -145,6 +159,7 @@ const playQueueSlice = createSlice({
decrementCurrentIndex: (state, action: PayloadAction<string>) => {
if (state.entry.length >= 1) {
state.currentSeek = 0;
if (state.currentIndex > 0) {
state.currentIndex -= 1;
if (action.payload === 'usingHotkey') {
@ -168,10 +183,19 @@ const playQueueSlice = createSlice({
},
setPlayQueue: (state, action: PayloadAction<Entry[]>) => {
// Reset player defaults
state.entry = [];
state.status = 'PAUSED';
state.currentIndex = 0;
state.currentSongId = '';
state.currentPlayer = 1;
state.currentSeek = 0;
state.player1.index = 0;
state.player2.index = 1;
if (state.status !== 'PLAYING') {
state.status = 'PLAYING';
}
state.currentIndex = 0;
state.currentSongId = action.payload[0].id;
action.payload.map((entry: any) => state.entry.push(entry));
},
@ -180,7 +204,16 @@ const playQueueSlice = createSlice({
action.payload.map((entry: any) => state.entry.push(entry));
},
clearPlayQueue: () => initialState,
clearPlayQueue: (state) => {
state.entry = [];
state.status = 'PAUSED';
state.currentIndex = 0;
state.currentSongId = '';
state.currentPlayer = 1;
state.currentSeek = 0;
state.player1.index = 0;
state.player2.index = 1;
},
setIsLoading: (state) => {
state.isLoading = true;
@ -269,5 +302,7 @@ export const {
setCurrentPlayer,
setStatus,
setPlayerVolume,
setVolume,
setCurrentSeek,
} = playQueueSlice.actions;
export default playQueueSlice.reducer;

24
yarn.lock

@ -1064,9 +1064,9 @@
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
version "7.14.8"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.8.tgz#7119a56f421018852694290b9f9148097391b446"
integrity sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg==
version "7.15.3"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.3.tgz#2e1c2880ca118e5b2f9988322bd8a7656a32502b"
integrity sha512-OvwMLqNXkCXSz1kSm58sEsNuhqOx/fKpnUnKnFB5v8uDda5bLNEHNgKPvhDN6IU0LDcnHQ90LlJ0Q6jnyBSIBA==
dependencies:
regenerator-runtime "^0.13.4"
@ -2247,6 +2247,11 @@ acorn@^8.0.4:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.0.4.tgz#7a3ae4191466a6984eee0fe3407a4f3aa9db8354"
integrity sha512-XNP0PqF1XD19ZlLKvB7cMmnZswW4C/03pRHgirB30uSJTaS3A3V1/P4sS3HPvFmjoriPCJQs+JDSbm4bL1TxGQ==
add-zero@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/add-zero/-/add-zero-1.0.0.tgz#88e221696717f66db467672f3f9aa004de9f1a2c"
integrity sha1-iOIhaWcX9m20Z2cvP5qgBN6fGiw=
address@^1.0.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/address/-/address-1.1.2.tgz#bf1116c9c758c51b7a933d296b72c221ed9428b6"
@ -5920,6 +5925,14 @@ form-data@~2.3.2:
combined-stream "^1.0.6"
mime-types "^2.1.12"
format-duration@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/format-duration/-/format-duration-1.4.0.tgz#6ad86c88f504150873cd28ca82d26a26898d9971"
integrity sha512-Mcg3hOAiKxo6JRBgfrQ+sbVvr3D9/4wE7eDQXx3WV2d/yKmrcHXHJS4OhrqVeg+iiFE2Op+pHhdOkQUl8yIclw==
dependencies:
add-zero "^1.0.0"
parse-ms "^1.0.1"
forwarded@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
@ -9341,6 +9354,11 @@ parse-json@^5.0.0:
json-parse-even-better-errors "^2.3.0"
lines-and-columns "^1.1.6"
parse-ms@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-1.0.1.tgz#56346d4749d78f23430ca0c713850aef91aa361d"
integrity sha1-VjRtR0nXjyNDDKDHE4UK75GqNh0=
parse5@5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178"

Loading…
Cancel
Save