Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: wavy progress bar #251

Merged
merged 17 commits into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@react-navigation/native-stack": "^6.9.17",
"@sharcoux/slider": "^7.0.5",
"@shopify/flash-list": "^1.6.3",
"@shopify/react-native-skia": "^0.1.225",
"axios": "^1.6.2",
"babel-plugin-transform-remove-console": "^6.9.4",
"base64-js": "^1.5.1",
Expand All @@ -48,6 +49,7 @@
"cheerio": "^1.0.0-rc.12",
"colord": "^2.9.3",
"crypto-js": "^4.1.1",
"d3": "^7.8.5",
"dayjs": "^1.11.9",
"deepmerge": "^4.3.1",
"dropbox": "git+https://lovegaoshi@github.com/lovegaoshi/dropbox-sdk-js.git",
Expand Down Expand Up @@ -81,7 +83,7 @@
"react-native-get-random-values": "^1.10.0",
"react-native-lyric": "git+https://github.com/lovegaoshi/react-native-lyric.git",
"react-native-pager-view": "^6.2.3",
"react-native-paper": "^5.11.3",
"react-native-paper": "^5.11.4",
"react-native-qrcode-svg": "^6.2.0",
"react-native-reanimated": "^3.6.1",
"react-native-safe-area-context": "^4.8.0",
Expand All @@ -98,7 +100,7 @@
"react-native-vector-icons": "^10.0.2",
"react-native-video": "^5.2.1",
"react-native-webview": "^13.6.3",
"react-native-windows": "^0.73.1",
"react-native-windows": "^0.73.2",
"react-use": "^17.4.2",
"use-debounce": "^10.0.0",
"uuid": "^9.0.0",
Expand All @@ -109,12 +111,13 @@
"metro": "^0.80.1"
},
"devDependencies": {
"@babel/core": "^7.23.2",
"@babel/preset-env": "^7.23.2",
"@babel/core": "^7.23.6",
"@babel/preset-env": "^7.23.6",
"@babel/preset-typescript": "^7.23.2",
"@babel/runtime": "^7.23.2",
"@babel/runtime": "^7.23.6",
"@react-native-community/eslint-config": "^3.2.0",
"@tsconfig/react-native": "^3.0.2",
"@types/d3": "^7.4.3",
"@types/jest": "^29.5.8",
"@types/md5": "^2.3.5",
"@types/node": "^20.9.0",
Expand All @@ -125,8 +128,8 @@
"@types/react-native-video": "^5.0.18",
"@types/react-test-renderer": "^18.0.6",
"@types/uuid": "^9.0.7",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
"@welldone-software/why-did-you-render": "^7.0.1",
"argparse": "^2.0.1",
"babel-jest": "^29.6.4",
Expand Down Expand Up @@ -155,4 +158,4 @@
},
"isSwift": true,
"packageManager": "yarn@1.22.19"
}
}
2 changes: 1 addition & 1 deletion src/AzusaPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { useTranslation } from 'react-i18next';

import { Player } from './components/player/View';
import Playlist from './components/playlist/View';
import PlayerBottomPanel from './components/player/PlayerProgressControls';
import PlayerBottomPanel from './components/player/controls/PlayerProgressControls';
import { useNoxSetting } from '@stores/useApp';
import PlaylistDrawer from './components/playlists/View';
import { ViewEnum } from './enums/View';
Expand Down
5 changes: 3 additions & 2 deletions src/components/landscape/LandscapePlayerProgress.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { View } from 'react-native';
import { useActiveTrack } from 'react-native-track-player';
import { Progress } from '@components/player/Progress';
import { Progress } from '@components/player/controls/Progress';
import PlayerControls from './PlayerControlsSquared';
import { useNoxSetting } from '@stores/useApp';
import { styles } from '../style';

interface Props {
panelWidth: number;
Expand All @@ -15,7 +16,7 @@ export default ({ panelWidth }: Props) => {
return (
<View
style={[
playerStyle.actionRowContainer,
styles.actionRowContainer,
{
backgroundColor: playerStyle.colors.background,
width: panelWidth,
Expand Down
10 changes: 5 additions & 5 deletions src/components/landscape/PlayerControlsSquared.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import React from 'react';
import { Dimensions, StyleSheet, View } from 'react-native';
import { usePlaybackState } from 'react-native-track-player';

import { PlaybackError } from '@components/player/PlaybackError';
import { PlayPauseButton } from '@components/player/PlayPauseButton';
import { PlaybackError } from '@components/player/controls/PlaybackError';
import { PlayPauseButton } from '@components/player/controls/PlayPauseButton';
import { useNoxSetting } from '@stores/useApp';
import ThumbsUpButton from '@components/player/ThumbsUpButton';
import PlayerModeButton from '@components/player/PlayerModeButton';
import usePlayerControls from '@components/player/usePlayerControls';
import ThumbsUpButton from '@components/player/controls/ThumbsUpButton';
import PlayerModeButton from '@components/player/controls/PlayerModeButton';
import usePlayerControls from '@components/player/controls/usePlayerControls';
import LottieButton from '../buttons/LottieButton';

interface Props {
Expand Down
2 changes: 1 addition & 1 deletion src/components/player/PIPLyric.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import TrackPlayer, { Track } from 'react-native-track-player';

import { useNoxSetting } from '@stores/useApp';
import { LyricView } from './Lyric';
import usePlayerControls from '@components/player/usePlayerControls';
import usePlayerControls from '@components/player/controls/usePlayerControls';

const PIPLyricView = () => {
const [currentTrack, setCurrentTrack] = useState<Track | undefined>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import TrackPlayer, {

import { useDebouncedValue } from 'hooks';
import { useNoxSetting } from '@stores/useApp';
import LottieButtonAnimated from '../buttons/LottieButtonAnimated';
import LottieButtonAnimated from '@components/buttons/LottieButtonAnimated';
import { fadePause } from '@utils/RNTPUtils';

interface Props {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { PlayPauseButton } from './PlayPauseButton';
import ThumbsUpButton from './ThumbsUpButton';
import PlayerModeButton from './PlayerModeButton';
import usePlayerControls from './usePlayerControls';
import LottieButton from '../buttons/LottieButton';
import LottieButton from '@components/buttons/LottieButton';

export const PlayerControls: React.FC = () => {
const { performSkipToNext, performSkipToPrevious } = usePlayerControls();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useStore } from 'zustand';

import noxPlayingList from '@stores/playingList';
import { cycleThroughPlaymode } from '@utils/RNTPUtils';
import ShadowedButton from '../buttons/ShadowedButton';
import ShadowedButton from '@components/buttons/ShadowedButton';

interface Props {
iconSize?: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useActiveTrack } from 'react-native-track-player';
import { Progress } from './Progress';
import { PlayerControls } from './PlayerControls';
import { useNoxSetting } from '@stores/useApp';
import { styles } from '@components/style';

export default () => {
const track = useActiveTrack();
Expand All @@ -11,7 +12,7 @@ export default () => {
return (
<View
style={[
playerStyle.actionRowContainer,
styles.actionRowContainer,
{ backgroundColor: playerStyle.colors.background },
]}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Slider } from '@sharcoux/slider';
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import TrackPlayer, { useProgress } from 'react-native-track-player';
import { ProgressBar } from 'react-native-paper';
import { useStore } from 'zustand';
import { Slider } from '@sharcoux/slider';

import ProgressWavy from './ProgressWavy';
import { useNoxSetting } from '@stores/useApp';
import { seconds2MMSS as formatSeconds } from '@utils/Utils';
import appStore from '@stores/appStore';
Expand All @@ -31,6 +32,7 @@ export const Progress: React.FC<{ live?: boolean }> = ({ live }) => {
</View>
) : (
<View style={styles.container}>
<ProgressWavy />
<View style={styles.progressContainer}>
<Slider
style={styles.progressBar}
Expand Down Expand Up @@ -95,6 +97,7 @@ const styles = StyleSheet.create({
progressContainer: {
width: '100%',
paddingHorizontal: 25,
marginTop: -20,
},
progressBar: {
width: '100%',
Expand Down
33 changes: 33 additions & 0 deletions src/components/player/controls/ProgressWavy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { usePlayWhenReady, useProgress } from 'react-native-track-player';

import WaveAnimation from './WavyAnimation';
import { useNoxSetting } from '@stores/useApp';

export default () => {
const playWhenReady = usePlayWhenReady();
const { position, duration } = useProgress();
const playerSetting = useNoxSetting(state => state.playerSetting);
const playerStyle = useNoxSetting(state => state.playerStyle);

return (
<View style={styles.waveProgressContainer}>
{playerSetting.wavyProgressBar && (
<WaveAnimation
playing={playWhenReady}
progress={position / duration}
color={playerStyle.customColors.progressMinimumTrackTintColor}
/>
)}
</View>
);
};

const styles = StyleSheet.create({
waveProgressContainer: {
width: '100%',
paddingHorizontal: 25,
height: 30,
},
});
114 changes: 114 additions & 0 deletions src/components/player/controls/WavyAnimation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { Dimensions, StyleSheet, View } from 'react-native';
import {
Canvas,
Path,
Skia,
useClockValue,
useComputedValue,
useValue,
LinearGradient,
vec,
} from '@shopify/react-native-skia';
import { line, curveBasis } from 'd3';
import { colord } from 'colord';

import { gaussian } from '@utils/Gaussian';

const dimension = Dimensions.get('window');
const width = dimension.width;
const height = 30;
const frequency = 2;
const initialAmplitude = 10;
const initialVerticalOffset = 10;

interface Props {
playing?: boolean;
progress?: number;
color?: string;
}
export default function WaveAnimation({
playing = false,
progress = 0,
color = 'white',
}: Props) {
const verticalOffset = useValue(initialVerticalOffset);
const amplitude = useValue(initialAmplitude);
const clock = useClockValue();
const extrapolatedWidth = Math.max(width * progress * 0.9 - 3, 0);
const parsedColor = colord(color);

const createWavePath = (phase = 20) => {
const points: [number, number][] = Array.from(
{ length: extrapolatedWidth },
(_, index) => {
const angle = (1 - index / width) * (Math.PI * frequency) + phase;
return [
index,
amplitude.current *
(Math.sin(angle) - 1) *
gaussian(index / extrapolatedWidth) +
verticalOffset.current +
17,
];
}
);
const lineGenerator = line().curve(curveBasis);
const waveLine = lineGenerator(points);
const bottomLine = `L${extrapolatedWidth},${height} L${0}, ${height}`;
return `${waveLine || bottomLine} ${bottomLine} Z`;
};

const animatedPath = useComputedValue(() => {
const current = (clock.current / 500) % 255;
const start = Skia.Path.MakeFromSVGString(createWavePath(current));
const end = Skia.Path.MakeFromSVGString(createWavePath(Math.PI * current));
return start!.interpolate(end!, 0.5)!;
}, [clock, verticalOffset, progress]);

const animatedPath2 = useComputedValue(() => {
const current = (clock.current / 700) % 255;
const start = Skia.Path.MakeFromSVGString(createWavePath(current));
const end = Skia.Path.MakeFromSVGString(createWavePath(Math.PI * current));
return start!.interpolate(end!, 0.5)!;
}, [clock, verticalOffset, progress]);

const gradientStart = useComputedValue(() => {
return vec(0, 0);
}, [width]);

const gradientEnd = useComputedValue(() => {
return vec(width, 0);
}, [width]);

if (!playing) return <></>;

return (
<View style={styles.container}>
<Canvas style={styles.canvas}>
<Path path={animatedPath} style={'fill'} color={color}>
{false && (
<LinearGradient
start={gradientStart}
end={gradientEnd}
colors={['cyan', 'blue']}
/>
)}
</Path>
<Path
path={animatedPath2}
style={'fill'}
color={parsedColor.alpha(0.9).toRgbString()}
></Path>
</Canvas>
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
},
canvas: {
flex: 1,
},
});
6 changes: 3 additions & 3 deletions src/components/player/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from './PlayerControls';
export * from './PlayPauseButton';
export * from './Progress';
export * from './controls/PlayerControls';
export * from './controls/PlayPauseButton';
export * from './controls/Progress';
26 changes: 13 additions & 13 deletions src/components/playlist/usePlaylistRN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,19 @@ export default (playlist: NoxMedia.Playlist) => {
}
};

/**
* playlistShouldReRender is a global state that indicates playlist should be
* refreshed. right now its only called when the playlist is updated in updatePlaylist.
* this should in turn clear all searching, checking and filtering.
*/
useEffect(() => {
resetSelected();
setChecking(false);
setSearching(false);
setRows(playlist.songList);
setCachedSongs(Array.from(noxCache.noxMediaCache.cache.keys()));
}, [playlist, playlistShouldReRender]);

useEffect(() => {
if (
playerSetting.autoRSSUpdate &&
Expand All @@ -145,19 +158,6 @@ export default (playlist: NoxMedia.Playlist) => {
}
}, [playlist]);

/**
* playlistShouldReRender is a global state that indicates playlist should be
* refreshed. right now its only called when the playlist is updated in updatePlaylist.
* this should in turn clear all searching, checking and filtering.
*/
useEffect(() => {
resetSelected();
setChecking(false);
setSearching(false);
setRows(playlist.songList);
setCachedSongs(Array.from(noxCache.noxMediaCache.cache.keys()));
}, [playlist, playlistShouldReRender]);

useEffect(() => handleSearch(debouncedSearchText), [debouncedSearchText]);

useEffect(() => {
Expand Down
Loading
Loading