Skip to content
Open
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
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
node_modules/**/*
.expo/*
npm-debug.*
*.jks
*.p8
Expand All @@ -21,3 +19,8 @@ web-report/
.env
.env.*
!.env.example

# Device-specific
/.expo/
/node_modules/

2 changes: 1 addition & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

lint-staged
yarn lint-staged
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Developing an app to enhance Ultimate coaching abilities

You need a recent version of nodejs.

Clone the project and install all its dependancies:
Clone the project and install all its dependencies:

```
git clone git@github.com:disc-in/UltimateApp.git
Expand All @@ -30,11 +30,11 @@ yarn start

### External dependencies

The Disc In app relies on **Firebase** for sharing drills and plays. For this reason, each play gets a unique "**share identier**" whoch is a 10 characters long hexadecimal string. It makes sure plays and drills uploaded on Firebase do not override one another. It is different from the **id/uuid** plays and drills have locally, used to manage the redux store and make sure each action is applied to the right record. This is necessary because users may download the same drill several times, or reshare a drill they have downloaded, without impacting other instances of the drill.
The Disc In app relies on **Firebase** for sharing drills and plays. For this reason, each play gets a unique "**share identifier**" which is a 10 characters long hexadecimal string. It makes sure plays and drills uploaded on Firebase do not override one another. It is different from the **id/uuid** plays and drills have locally, used to manage the redux store and make sure each action is applied to the right record. This is necessary because users may download the same drill several times, or reshare a drill they have downloaded, without impacting other instances of the drill.

## 👏 Contributing

If you want to contribute to the projet, just pick up an issue from the [list](https://github.com/disc-in/UltimateApp/issues) and start fixing it. You can then open a pull-request so that your contribution can be merged into the main branch.
If you want to contribute to the project, just pick up an issue from the [list](https://github.com/disc-in/UltimateApp/issues) and start fixing it. You can then open a pull-request so that your contribution can be merged into the main branch.

Found a bug? Take 5 minutes to [report it](https://github.com/disc-in/UltimateApp/issues/new?assignees=&labels=bug&template=bug_report.md&title=)

Expand Down
9 changes: 5 additions & 4 deletions app.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default {
slug: 'ultimate-app',
privacy: 'public',
platforms: ['ios', 'android'],
version: '2.0.0',
version: '2.1.0',
githubUrl: 'https://github.com/disc-in/UltimateApp',
orientation: 'portrait',
scheme: 'discin',
Expand All @@ -35,20 +35,21 @@ export default {
runtimeVersion: {
policy: 'sdkVersion',
},
newArchEnabled: true,
ios: {
bundleIdentifier: 'com.discin.discin',
buildNumber: '2.0.0',
buildNumber: '2.1.0',
supportsTablet: true,
infoPlist: {
CFBundleAllowMixedLocalizations: true,
},
},
android: {
package: 'com.discin.discin',
versionCode: 9,
versionCode: 10,
permissions: [],
},
plugins: ['expo-localization'],
plugins: ['expo-localization', 'expo-audio'],
extra: {
eas: {
projectId: 'e2242930-ad3a-4007-afc1-f9c47c174979',
Expand Down
14 changes: 4 additions & 10 deletions metro.config.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
const { getDefaultConfig } = require('metro-config');
const { getDefaultConfig } = require('expo/metro-config');

module.exports = (async () => {
const {
resolver: { assetExts, sourceExts },
} = await getDefaultConfig();

return {
resolver: {
sourceExts: [...sourceExts, 'cjs', 'mjs'],
},
};
const config = await getDefaultConfig(__dirname);
config.resolver.sourceExts.push('cjs', 'mjs');
return config;
})();
74 changes: 45 additions & 29 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,64 +3,78 @@
"test": "jest --watch --coverage=false --changedSince=main",
"test-ci": "jest --coverage=true",
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"android": "expo run:android",
"ios": "expo run:ios",
"prepare": "husky install"
},
"dependencies": {
"@react-native-async-storage/async-storage": "1.18.2",
"@react-native-masked-view/masked-view": "0.2.9",
"@react-native-picker/picker": "2.4.10",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.0.0-0",
"@babel/plugin-proposal-optional-chaining": "^7.21.4-esm.4",
"@babel/plugin-transform-arrow-functions": "^7.0.0-0",
"@babel/plugin-transform-shorthand-properties": "^7.27.1",
"@babel/plugin-transform-template-literals": "^7.27.1",
"@babel/preset-env": "^7.28.3",
"@react-native-async-storage/async-storage": "2.1.2",
"@react-native-masked-view/masked-view": "0.3.2",
"@react-native-picker/picker": "2.11.1",
"@react-navigation/material-bottom-tabs": "^6.2.19",
"@react-navigation/native": "^6.1.9",
"@react-navigation/stack": "^6.3.20",
"dotenv": "^16.0.1",
"expo": "^49.0.0",
"expo-av": "~13.4.1",
"expo-constants": "~14.4.2",
"expo-crypto": "~12.4.1",
"expo-linking": "~5.0.2",
"expo-localization": "~14.3.0",
"expo-mail-composer": "~12.3.0",
"expo-screen-orientation": "~6.0.6",
"expo-splash-screen": "~0.20.5",
"expo-updates": "~0.18.17",
"expo": "53.0.22",
"expo-audio": "~0.4.9",
"expo-constants": "~17.1.7",
"expo-crypto": "~14.1.5",
"expo-linking": "~7.1.7",
"expo-localization": "~16.1.6",
"expo-mail-composer": "~14.1.6",
"expo-screen-orientation": "~8.1.7",
"expo-splash-screen": "~0.30.10",
"expo-updates": "~0.28.17",
"expo-video": "^2.2.2",
"firebase": "9.17.1",
"formik": "^2.2.9",
"i18n-js": "^4.3.2",
"react": "18.2.0",
"react-native": "0.72.6",
"prop-types": "^15.8.1",
"react": "19.0.0",
"react-native": "0.79.5",
"react-native-collapsible": "^1.6.0",
"react-native-elements": "^3.4.3",
"react-native-flash-message": "^0.4.0",
"react-native-gesture-handler": "~2.12.0",
"react-native-pager-view": "6.2.0",
"react-native-paper": "^5.11.3",
"react-native-reanimated": "~3.3.0",
"react-native-safe-area-context": "4.6.3",
"react-native-screens": "~3.22.0",
"react-native-gesture-handler": "~2.24.0",
"react-native-pager-view": "6.7.1",
"react-native-paper": "^5.14.5",
"react-native-reanimated": "~3.17.4",
"react-native-safe-area-context": "5.4.0",
"react-native-screens": "~4.11.1",
"react-native-snap-carousel": "yadigarbz/react-native-snap-carousel#df1ec4da3194eb20ffb797270b971a33b3ae6d72",
"react-native-vector-icons": "^10.3.0",
"react-native-vimeo-iframe": "^1.2.1",
"react-native-webview": "13.13.5",
"react-navigation-material-bottom-tabs": "^2.3.5",
"react-redux": "^8.0.5",
"react-test-renderer": "18.2.0",
"redux": "^4.2.0",
"redux-persist": "^6.0.0",
"watchman": "^1.0.0",
"yup": "^0.32.11"
},
"devDependencies": {
"@babel/core": "^7.19.3",
"@babel/core": "^7.24.0",
"@babel/traverse": "^7.28.3",
"@testing-library/react-native": "^11.5.0",
"babel-preset-expo": "^9.5.0",
"babel-preset-expo": "~13.0.0",
"eslint": "^8.31.0",
"eslint-config-universe": "^11.1.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-native": "^4.0.0",
"husky": "^8.0.0",
"jest": "^29.4.3",
"jest-expo": "~49.0.0",
"jest": "~29.7.0",
"jest-expo": "~53.0.10",
"lint-staged": "^13.1.0",
"prettier": "^2.8.2",
"redux-mock-store": "^1.5.4",
"typescript": "^5.1.3"
"typescript": "~5.8.3"
},
"lint-staged": {
"*.js": "eslint --cache --fix",
Expand All @@ -80,5 +94,7 @@
"node": ">=0.20"
},
"private": true,
"license": "MIT"
"license": "MIT",
"name": "UltimateApp",
"version": "1.0.0"
}
119 changes: 38 additions & 81 deletions src/Components/shared/VimeoVideo.js
Original file line number Diff line number Diff line change
@@ -1,72 +1,33 @@
import React, { useEffect, useRef, useState } from 'react';
import React, { useState } from 'react';
import { View, ActivityIndicator, StyleSheet, Text } from 'react-native';
import { Video, Audio, VideoFullscreenUpdate, ResizeMode } from 'expo-av';
import * as ScreenOrientation from 'expo-screen-orientation';
import { useFocusEffect } from '@react-navigation/native';

import { Vimeo } from 'react-native-vimeo-iframe';
import I18n from '../../utils/i18n';
import theme from '../../styles/theme.style';

const VimeoVideo = ({ vimeoId, sounds, shouldPlay }) => {
const videoElem = useRef(null);
const [isBuffering, setBuffer] = useState(true);
const [error, setError] = useState();

useEffect(() => {
const vimeoUrlSource = `https://player.vimeo.com/video/${vimeoId}/config`;
let aborted = false;
setBuffer(true);
setError(null);

fetch(vimeoUrlSource)
.then((res) => res.json())
.then((res) => {
const videoArray = res.request.files.progressive;
const videoVimeoQuality = videoArray.find((videoObject) => videoObject.quality === '540p');
if (videoVimeoQuality) {
return videoVimeoQuality.url;
}
})
.then((url) => {
if (aborted) return;
return videoElem.current.loadAsync({
uri: url,
});
})
.catch((e) => {
if (aborted) return;
setError(e);
setBuffer(false);
});

return () => (aborted = true);
}, [vimeoId]);
const VimeoVideo = ({ vimeoId, sounds, shouldPlay = true }) => {
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(false);

// Stop playing the video on screen change
useFocusEffect(
React.useCallback(() => {
return () => {
videoElem.current?.pauseAsync();
};
}, []),
);
// Vimeo player options
const videoOptions = {
autoplay: shouldPlay,
loop: true,
muted: !sounds, // Invert sounds to get muted
};

const playVideoLoaded = () => {
Audio.setAudioModeAsync({
playsInSilentModeIOS: true,
});
videoElem.current.setStatusAsync({
rate: 1.0,
isMuted: !sounds,
resizeMode: ResizeMode.CONTAIN,
shouldPlay: shouldPlay || false,
isLooping: true,
});
const onError = (errorMessage) => {
console.error('Vimeo video error:', errorMessage);
setError(true);
setIsLoading(false);
};

setBuffer(false);
const onReady = () => {
setIsLoading(false);
};

const renderBufferIcon = () => {
const renderLoader = () => {
if (!isLoading) return null;

return (
<View style={styles.spinnerStyle}>
<ActivityIndicator animating color={theme.COLOR_SECONDARY} size="large" />
Expand All @@ -76,6 +37,8 @@ const VimeoVideo = ({ vimeoId, sounds, shouldPlay }) => {
};

const renderError = () => {
if (!error) return null;

return (
<View style={styles.spinnerStyle}>
<Text>{I18n.t('vimeoVideo.error')}</Text>
Expand All @@ -85,37 +48,31 @@ const VimeoVideo = ({ vimeoId, sounds, shouldPlay }) => {

return (
<View style={styles.videoContainer}>
{error && renderError()}
{isBuffering && renderBufferIcon()}
<Video
ref={videoElem}
resizeMode={ResizeMode.CONTAIN}
useNativeControls
style={{ width: '100%', height: 250 }}
onLoadStart={() => setBuffer(true)}
onLoad={playVideoLoaded}
onFullscreenUpdate={async ({ fullscreenUpdate }) => {
if (fullscreenUpdate === VideoFullscreenUpdate.PLAYER_WILL_PRESENT) {
await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE_LEFT);
}
if (fullscreenUpdate === VideoFullscreenUpdate.PLAYER_WILL_DISMISS) {
await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT);
}
}}
/>
{renderLoader()}
{renderError()}
<Vimeo videoId={vimeoId} onReady={onReady} onError={onError} style={styles.video} options={videoOptions} />
</View>
);
};

const styles = StyleSheet.create({
videoContainer: {
flex: 1,
height: 250,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#000',
},
video: {
width: '100%',
height: 250,
},
spinnerStyle: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#000',
},
});

Expand Down
8 changes: 3 additions & 5 deletions src/Screens/DrillPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const DrillPage = (props) => {

// Create Component refs
const drillScrollView = useRef(null);
const descriptionRef = useRef(null);
const descriptionY = useRef(0);

// Get Header Height
const headerHeight = useHeaderHeight();
Expand All @@ -50,9 +50,7 @@ export const DrillPage = (props) => {
if (drill.type === DrillTypes.FITNESS) {
startFitness();
} else {
descriptionRef.current.measureLayout(findNodeHandle(drillScrollView.current), (x, y) => {
drillScrollView.current.scrollTo({ x: 0, y, animated: true });
});
drillScrollView.current.scrollTo({ y: descriptionY.current, animated: true });
}
};

Expand Down Expand Up @@ -108,7 +106,7 @@ export const DrillPage = (props) => {
</View>
</View>
</ImageBackground>
<View ref={descriptionRef}>
<View onLayout={(event) => (descriptionY.current = event.nativeEvent.layout.y)}>
<Description drill={drill} />
</View>
<View style={styles.animation}>
Expand Down
Loading