diff --git a/.eslintrc.json b/.eslintrc.json index dbb66640..f7971cda 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -2,6 +2,7 @@ "extends": ["airbnb", "prettier", "plugin:prettier/recommended", "eslint-config-prettier"], "parser": "@babel/eslint-parser", "rules": { + "react-native/no-unused-styles": 2, "curly": "warn", "import/no-unresolved": "off", "global-require": 0, @@ -25,7 +26,87 @@ "trailingComma": "es5", "printWidth": 100 } + ], + "import/order": [ + "error", + { + "groups": ["builtin", "external", "internal", "parent", "sibling", "index"], + "pathGroups": [ + { + "pattern": "react*", + "group": "external", + "position": "before" + }, + { + "pattern": "@common/**", + "group": "internal", + "position": "before" + }, + { + "pattern": "@**", + "group": "internal", + "position": "before" + } + ], + "pathGroupsExcludedImportTypes": ["builtin"], + "newlines-between": "never", + "alphabetize": { + "order": "asc", + "caseInsensitive": true + } + } ] }, - "plugins": ["prettier"] + "overrides": [ + { + "files": ["**/*.test.js", "**/*.test.jsx", "**/__mocks__/**", "**/test-utils/**"], + "env": { + "jest": true + }, + "rules": { + "react/jsx-filename-extension": "off", + "import/prefer-default-export": "off", + "react/prop-types": "off", + "no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], + "react/react-in-jsx-scope": "off", + "react/jsx-no-useless-fragment": "off", + "no-nested-ternary": "off" + } + }, + { + "files": ["**/*.test.jsx"], + "rules": { + "import/order": [ + "error", + { + "groups": ["builtin", "external", "internal", "parent", "sibling", "index"], + "pathGroups": [ + { + "pattern": "react*", + "group": "external", + "position": "before" + }, + { + "pattern": "@common/**", + "group": "internal", + "position": "before" + }, + { + "pattern": "@**", + "group": "internal", + "position": "before" + } + ], + "pathGroupsExcludedImportTypes": ["builtin"], + "newlines-between": "always", + "alphabetize": { + "order": "asc", + "caseInsensitive": true + } + } + ] + } + } + ], + "plugins": ["prettier", "react-native"] } diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index c696c3dc..af7bba82 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -4,5 +4,5 @@ contact_links: url: https://github.com/KhalisFoundation/sundar-gutka-react/blob/master/README.md about: Please check our documentation before creating an issue. - name: ๐Ÿ› Known Issues - url: https://github.com/WahegurooNetwork/SundarGutka/issues?q=is%3Aissue+is%3Aopen+label%3Abug + url: https://github.com/KhalisFoundation/sundar-gutka-react/issues?q=is%3Aissue+is%3Aopen+label%3Abug about: Check if your issue has already been reported. diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml new file mode 100644 index 00000000..d30459a0 --- /dev/null +++ b/.github/workflows/pr-tests.yml @@ -0,0 +1,36 @@ +name: PR CI + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + branches: [master, dev] + +concurrency: + group: pr-ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint-and-test: + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "yarn" + + - name: Install dependencies + run: yarn install --frozen-lockfile + + # Lint step + - name: Run ESLint + run: yarn lint + + # Unit tests (Jest example) + - name: Run tests + run: yarn test --ci --watchAll=false --reporters=default diff --git a/.gitignore b/.gitignore index 077202fa..d3057780 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ local.properties *.keystore !debug.keystore .kotlin/ +android/node # node.js # diff --git a/.vscode/settings.json b/.vscode/settings.json index 5fd5f475..82e13719 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,5 +6,25 @@ "prettier.semi": true, "javascript.format.semicolons": "insert", "prettier.printWidth": 100, - "cSpell.words": ["Pressable"] + "cSpell.words": ["Larivaar", "padched", "Pressable", "shabad", "vishraam"], + "react-native-ide.jsxRuntime": "automatic", + "react-native-ide.reactVersion": "19.0.0", + "react-native-ide.reactNativeVersion": "0.78.0", + "typescript.preferences.includePackageJsonAutoImports": "on", + "javascript.preferences.includePackageJsonAutoImports": "on", + "react-native-ide.enableTypeScript": true, + "react-native-ide.enableJavaScript": true, + "react-native-ide.enableJSX": true, + "react-native-ide.moduleResolution": "node", + "react-native-ide.allowSyntheticDefaultImports": true, + "react-native-ide.esModuleInterop": true, + "react-native-ide.enableRenderer": false, + "react-native-ide.enableFabric": false, + "react-native-ide.enableNewArchitecture": false, + "react-native-ide.compatibilityMode": true, + "react-native-ide.moduleMapping": { + "__RNIDE_lib__/JSXRuntime/react-native-78-79/react-jsx-dev-runtime.development.js": "react/jsx-dev-runtime", + "__RNIDE_lib__/rn-renderer/react-native-78-79/ReactFabric-dev.js": "react-native/Libraries/Renderer/implementations/ReactFabric-dev.js" + }, + "java.configuration.updateBuildConfiguration": "interactive" } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..fabef89e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,106 @@ +# Contributing to Sundar Gutka + +First of all, thank you for taking the time to contribute! + +The following is a set of guidelines for contributing to Sundar Gutka, which is hosted in the Khalis Foundation organization on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. + +## Recommendation + +Always write code using functional components (hooks based) following latest React and React-native standards. + +## Styleguides + +### Git Workflow + +We are currently following conventional commit style: + +1. **Branch Naming**: Use descriptive branch names (`feature/`, `fix/`, `refactor/`) +2. **Small PRs**: Keep pull requests focused and small +3. **Commit Messages**: Follow conventional commit format +4. **Review**: All PRs require review before merging + +### JavaScript Styleguide + +All JavaScript must adhere to our ESLint and Prettier rules. We recommend using VSCode with Prettier plugin installed to avoid linting errors. We anyway lint the code before pushing to repo. + +## Testing + +### Writing Tests + +**All new features and bug fixes must include tests.** Tests are required for: + +--- + +- New components +- New hooks +- New utility functions +- Bug fixes (regression tests) + +### Test Structure + +- Place test files next to the component/function they test: `ComponentName.test.jsx` or `hookName.test.js` +- Use Jest and React Testing Library for component testing +- Use the test utilities from `@common/test-utils` for mocking + +### Running Tests + +```bash +# Run all tests +yarn test +``` + +### Test Utilities + +See [`src/common/test-utils/README.md`](src/common/test-utils/README.md) for available mocks and utilities. + +## React Native Best Practices + +### Component Structure + +1. **Use Functional Components**: Always use functional components with hooks +2. **Memoization**: Use `React.memo()` for components that receive stable props +3. **Custom Hooks**: Extract reusable logic into custom hooks +4. **Component Organization**: Keep components small and focused on a single responsibility + +### State Management + +1. **Redux**: Use Redux Toolkit for global state management +2. **Local State**: Use `useState` for component-specific state +3. **Context**: Use React Context sparingly, prefer Redux for shared state + +### Navigation + +1. **Type Safety**: Use TypeScript types for navigation params when possible +2. **Deep Linking**: Consider deep linking when adding new screens +3. **Back Handler**: Use `useBackHandler` hook for Android back button handling + +### Styling + +1. **Themed Styles**: Always use `useThemedStyles` hook for styling +2. **StyleSheet**: Use `StyleSheet.create()` for performance +3. **Responsive Design**: Consider different screen sizes and orientations + +### Error Handling + +1. **Error Boundaries**: Wrap components in error boundaries where appropriate +2. **Try-Catch**: Use try-catch for async operations +3. **User Feedback**: Show user-friendly error messages + +### Accessibility + +1. **Accessibility Labels**: Add `accessibilityLabel` props to interactive elements +2. **Test IDs**: Use `testID` for testing purposes +3. **Screen Reader**: Test with screen readers (VoiceOver/TalkBack) + +### Code Organization + +1. **File Naming**: Use PascalCase for components, camelCase for utilities +2. **Import Order**: Follow ESLint import order rules +3. **Barrel Exports**: Use index.js files for clean imports +4. **Path Aliases**: Use `@common`, `@database`, `@service`, etc. for imports + +### Dependencies + +1. **Keep Updated**: Regularly update dependencies +2. **Native Modules**: Test native module changes on both platforms +3. **Lock File**: Commit `yarn.lock` to version control diff --git a/README.md b/README.md index 190d13d7..d8205307 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,242 @@ -# sundar-gutka-react +
+ Sundar Gutka Logo +
-### Getting Started +# Sundar Gutka -- `git clone https://github.com/KhalisFoundation/sundar-gutka-react.git`. -- `cd sundar-gutka-react`. -- `npm install`. +![Platform](https://img.shields.io/badge/platform-iOS%20%7C%20Android-blue) +[![Slack](https://img.shields.io/badge/Slack-Join%20Community-4A154B?logo=slack&logoColor=white)](https://khalis.slack.com) +![React Native](https://img.shields.io/badge/React%20Native-0.78+-61dafb) +![Yarn](https://img.shields.io/badge/package%20manager-yarn-2188b6) -### Android +Sundar Gutka is a feature-rich mobile application that provides access to Gurbani with extensive customization options for reading preferences, audio playback, translations, and more. The app supports both iOS and Android platforms and offers a seamless experience for daily Paath. -To setup android development environment: https://reactnative.dev/docs/environment-setup +## โœจ Features -Post environment setup +#### Reading Features -- Start the application - `npx react-native run-android`. -- Start metro bundler `npx react-native start`. +- **Multiple Font Options**: Choose from various Gurbani fonts including GurbaniAkharTrue, GurbaniAkharThickTrue, BalooPaaji, AnmolLipi, and more +- **Adjustable Font Size**: Five size options from Extra Small to Extra Large +- **Larivaar Mode**: Read Gurbani in continuous text format with optional assist mode +- **Paragraph Mode**: Toggle between traditional and paragraph formatting +- **Vishraam Options**: Color-coded or gradient punctuation marks for better reading flow +- **Auto Scroll**: Automatic scrolling synchronized with audio playback +- **Bookmarks**: Save and quickly navigate to your favorite Shabads +- **Position Saving**: Automatically saves your reading position for each Bani -### IOS +#### Translation & Transliteration -To setup ios development environment: https://reactnative.dev/docs/environment-setup +- **Multiple Languages**: Support for English, Hindi, Punjabi, Spanish, French, Italian, and more +- **Transliteration**: Romanized text options (English, Hindi, Shahmukhi, IPA) +- **Translations**: English, Punjabi, and Spanish translations available +- **Multi-language UI**: Interface available in multiple languages -Post environment setup +#### Audio Features -- Start the application - `npx react-native run-ios`. -- Start metro bundler `npx react-native start`. +- **Audio Player**: Built-in audio playback with React Native Track Player +- **Audio Sync**: Synchronized scrolling with audio playback +- **Background Playback**: Continue listening when app is in background +- **Auto Play**: Automatic audio playback option +- **Default Audio Selection**: Choose preferred audio source + +#### Customization Options + +- **Theme Support**: Light and Dark themes +- **Bani Order**: Customize the order of Banis in your Gutka +- **Bani Length**: Select from different lengths (SGPC, Taksal, Medium, Long, Extra Long) for major Banis +- **Keep Screen Awake**: Prevent screen from sleeping during reading +- **Status Bar Control**: Show or hide status bar + +#### Additional Features + +- **Folders**: Organize Banis into folders +- **Reminders**: Set up notification reminders for daily Paath +- **Database Updates**: In-app database update functionality +- **Statistics**: Optional usage statistics collection +- **Donation Support**: Support the Khalis Foundation + +## ๐Ÿš€ Getting Started + +### Prerequisites + +- **Node.js**: >= 18 +- **Package Manager**: Yarn (recommended) +- **React Native CLI**: Follow the [React Native environment setup guide](https://reactnative.dev/docs/environment-setup) + +### Installation + +1. Clone the repository: + + ```bash + git clone https://github.com/KhalisFoundation/sundar-gutka-react.git + cd sundar-gutka-react + ``` + +2. Install dependencies: + + ```bash + yarn install + ``` + +## ๐Ÿ“ฑ Platform Setup + +### Android Development + +1. **Environment Setup**: Follow the [React Native Android setup guide](https://reactnative.dev/docs/environment-setup) + +2. **Run the application**: + + ```bash + yarn android + ``` + +3. **Start Metro Bundler** (if not started automatically): + + ```bash + yarn start + ``` + +### iOS Development + +1. **Environment Setup**: Follow the [React Native iOS setup guide](https://reactnative.dev/docs/environment-setup) + +2. **Install CocoaPods dependencies**: + + ```bash + cd ios + pod install + cd .. + ``` + +3. **Run the application**: + + ```bash + yarn ios + ``` + +4. **Start Metro Bundler** (if not started automatically): + + ```bash + yarn start + ``` + +## ๐Ÿ—๏ธ Project Structure + +For detailed project structure information, see [PROJECT_STRUCTURE.md](docs/PROJECT_STRUCTURE.md). + +## ๐Ÿ› ๏ธ Key Technologies + +- **[React Native](https://github.com/facebook/react-native)**: ^0.78.0 +- **[React](https://github.com/facebook/react)**: 19.0.0 +- **[Redux Toolkit](https://github.com/reduxjs/redux-toolkit)**: State management +- **[React Navigation](https://github.com/react-navigation/react-navigation)**: Navigation library +- **[React Native Track Player](https://github.com/doublesymmetry/react-native-track-player)**: Audio playback +- **[React Native SQLite Storage](https://github.com/andpor/react-native-sqlite-storage)**: Local database +- **[Firebase](https://github.com/firebase/firebase-js-sdk)**: Analytics, Crashlytics, Messaging, Performance +- **[Anvaad JS](https://github.com/KhalisFoundation/anvaad-js)**: Gurbani transliteration library +- **[React Native WebView](https://github.com/react-native-webview/react-native-webview)**: HTML rendering for Gurbani text + +## ๐Ÿ“ Available Scripts + +- `start`: Start Metro bundler with ESLint +- `android`: Run Android app with ESLint +- `ios`: Run iOS app with ESLint +- `lint`: Run ESLint +- `test`: Run tests + +## โš™๏ธ Configuration + +### Firebase Setup + +The app uses Firebase for: + +- Analytics +- Crashlytics +- Push Notifications (Messaging) +- Performance Monitoring + +Ensure `google-services.json` (Android) and `GoogleService-Info.plist` (iOS) are properly configured. + +### Database + +The app uses SQLite for local storage. Database files are located in: + +- iOS: `ios/www/gutka_v01.db` +- Android: Bundled with the app + +## ๐ŸŽจ Customization + +### Themes + +The app supports light and dark themes. Theme configuration is located in `src/theme/`. + +### Fonts + +Custom fonts are located in `assets/fonts/`. Supported fonts include: + +- GurbaniAkharTrue +- GurbaniAkharThickTrue +- GurbaniAkharHeavyTrue +- BalooPaaji2-Regular +- BalooPaaji2-SemiBold +- AnmolLipiSG + +### Localization + +Localization strings are managed in `src/common/localization.js`. The app supports multiple languages for the UI. + +## ๐Ÿงช Testing + +Run tests with: + +```bash +yarn test +``` + +## ๐Ÿค Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +For detailed contribution guidelines, please see [CONTRIBUTING.md](CONTRIBUTING.md). + +**Before raising a pull request, please go through CONTRIBUTING.md.** We use `dev` branch as the development branch, while `master` is the production branch. You should branch out from `dev` branch and raise a PR against `dev` branch. + +1. Fork the repository +2. Create your feature branch from `dev` (`git checkout -b feature/AmazingFeature dev`) +3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) +4. Push to the branch (`git push origin feature/AmazingFeature`) +5. Open a Pull Request against the `dev` branch + +## ๐Ÿ“„ License + +This project is maintained by the [Khalis Foundation](https://khalisfoundation.org). + +## ๐Ÿ™ Acknowledgments + +- **BaniDB**: Sundar Gutka utilizes the open source Gurbani database and API used in many Gurbani applications, such as SikhiToTheMax +- **Khalis Foundation**: For maintaining and supporting this project + +## ๐Ÿ“ž Support + +For information, suggestions, or help, visit: + +- [Khalis Foundation](https://khalisfoundation.org) +- [BaniDB](https://www.banidb.com/) +- [Slack Channel](https://khalis.slack.com) - Join our community for discussions and support + +## โš ๏ธ Important Notes + +- Please respectfully cover your head and remove your shoes when using this app +- The app respects different sampardhas (traditions) and provides options for various Bani lengths while maintaining SGPC/Akaal Takht standards +- Bhul Chuk Maaf! (Please forgive any mistakes) + +--- + +
+ + Khalis Foundation + + + BaniDB + +
diff --git a/__mocks__/@react-native/js-polyfills/error-guard.js b/__mocks__/@react-native/js-polyfills/error-guard.js new file mode 100644 index 00000000..5fce8839 --- /dev/null +++ b/__mocks__/@react-native/js-polyfills/error-guard.js @@ -0,0 +1,7 @@ +// Mock for @react-native/js-polyfills/error-guard +// This avoids parsing Flow type syntax in Jest + +module.exports = { + setGlobalErrorHandler: jest.fn(), + getGlobalErrorHandler: jest.fn(() => null), +}; diff --git a/__tests__/App-test.js b/__tests__/App-test.js deleted file mode 100644 index 48c93a85..00000000 --- a/__tests__/App-test.js +++ /dev/null @@ -1,3 +0,0 @@ -/** - * @format - */ diff --git a/android/app/build.gradle b/android/app/build.gradle index 08a9a397..15af404e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -86,8 +86,8 @@ android { applicationId = "com.WahegurooNetwork.SundarGutka" minSdkVersion = rootProject.ext.minSdkVersion targetSdkVersion = rootProject.ext.targetSdkVersion - versionCode = 162 - versionName = "5.8.2" + versionCode = 170 + versionName = "5.9" ndk { abiFilters "arm64-v8a", "armeabi-v7a", "x86", "x86_64" } diff --git a/android/app/release/app-release.apk b/android/app/release/app-release.apk deleted file mode 100644 index 9d8ae0f2..00000000 Binary files a/android/app/release/app-release.apk and /dev/null differ diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml deleted file mode 100644 index 7a44a92a..00000000 --- a/android/app/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 55365840..7c777edd 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -2,9 +2,17 @@ xmlns:tools="http://schemas.android.com/tools"> + + + + android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="false" android:theme="@style/AppTheme" android:supportsRtl="true" + android:networkSecurityConfig="@xml/network_security_config"> + @@ -16,5 +24,10 @@ + + diff --git a/android/app/src/main/assets/fonts/BalooPaaji2-Regular.ttf b/android/app/src/main/assets/fonts/BalooPaaji2-Regular.ttf new file mode 100644 index 00000000..fef04c40 Binary files /dev/null and b/android/app/src/main/assets/fonts/BalooPaaji2-Regular.ttf differ diff --git a/android/app/src/main/assets/fonts/BalooPaaji2-SemiBold.ttf b/android/app/src/main/assets/fonts/BalooPaaji2-SemiBold.ttf new file mode 100644 index 00000000..d2404af3 Binary files /dev/null and b/android/app/src/main/assets/fonts/BalooPaaji2-SemiBold.ttf differ diff --git a/android/app/src/main/res/drawable/scrollbar_vertical_thumb.xml b/android/app/src/main/res/drawable/scrollbar_vertical_thumb.xml new file mode 100644 index 00000000..6b2d83c9 --- /dev/null +++ b/android/app/src/main/res/drawable/scrollbar_vertical_thumb.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index 00d2e9c6..66a81c7c 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -6,6 +6,7 @@ @color/primary_dark @color/primary_variant @color/primary_accent + @drawable/scrollbar_vertical_thumb - + ${content} diff --git a/src/ReaderScreen/utils/index.js b/src/ReaderScreen/utils/index.js index 1b3fa4fb..ed580906 100644 --- a/src/ReaderScreen/utils/index.js +++ b/src/ReaderScreen/utils/index.js @@ -1,19 +1,17 @@ -import { Platform } from "react-native"; -import { colors, constant, baseFontSize, logError, logMessage } from "@common"; +import { constant, baseFontSize, logError, logMessage } from "@common"; import htmlTemplate from "./gutkahtml"; import script from "./gutkaScript"; -export const fontColorForReader = (header, nightMode, text) => { - const { HEADER_COLOR_1_DARK, HEADER_COLOR_1_LIGHT, WHITE_COLOR, NIGHT_BLACK } = colors; +export const fontColorForReader = (header, theme, text) => { const { GURMUKHI, TRANSLATION, TRANSLITERATION } = constant; - const getHeaderColor1 = () => (nightMode ? HEADER_COLOR_1_DARK : HEADER_COLOR_1_LIGHT); - const getHeaderColor2 = () => (nightMode ? WHITE_COLOR : NIGHT_BLACK); + const getHeaderColor1 = () => theme.colors.primaryHeaderVariant; + const getHeaderColor2 = () => theme.colors.primaryText; const defaultColor = getHeaderColor2(); const gurmukhiMapping = { 1: getHeaderColor1(), - 2: defaultColor, + 2: getHeaderColor1(), 6: defaultColor, default: defaultColor, }; @@ -53,9 +51,10 @@ export const createDiv = ( type, textAlign, fontSize, - isNightMode, + theme, isLarivaar, - punjabiTranslation = "" + punjabiTranslation = "", + fontFace = null ) => { const fontClass = type === constant.GURMUKHI.toLowerCase() || punjabiTranslation !== "" @@ -66,7 +65,7 @@ export const createDiv = ( fontSize, header, type === constant.TRANSLITERATION.toLowerCase() || type === constant.TRANSLATION.toLowerCase() - )}px; color: ${fontColorForReader(header, isNightMode, type.toUpperCase())};"> + )}px; font-family: ${fontFace}; color: ${fontColorForReader(header, theme, type.toUpperCase())};"> ${content} `; @@ -80,38 +79,40 @@ export const loadHTML = ( isEnglishTranslation, isPunjabiTranslation, isSpanishTranslation, - isNightMode, - isLarivaar, - savePosition + theme, + isLarivaar ) => { try { - const backColor = isNightMode ? colors.NIGHT_BLACK : colors.WHITE_COLOR; - const fileUri = Platform.select({ - ios: `${fontFace}.ttf`, - android: `file:///android_asset/fonts/${fontFace}.ttf`, - }); - + const backColor = theme.colors.surface; const content = shabad .map((item) => { const textAlignMap = { 0: "left", - 1: "center", - 2: "center", + 1: "left", + 2: "left", }; let textAlign = textAlignMap[item.header]; if (textAlign === undefined) { textAlign = "right"; } - let contentHtml = `
`; + // Use pipe delimiters for easy CSS selector matching + const paragraphId = item.sequences ? item.sequences[0] : item.sequence; + const sequencesData = item.sequences + ? ` data-sequences='|${item.sequences.join("|")}|'` + : ""; + const sequenceData = ` data-sequence='${paragraphId}'`; + let contentHtml = `
`; contentHtml += createDiv( - item.gurmukhi, + fontFace === constant.BALOO_PAAJI ? item.gurmukhiUni : item.gurmukhi, item.header, constant.GURMUKHI.toLowerCase(), textAlign, fontSize, - isNightMode, - isLarivaar + theme, + isLarivaar, + "", + fontFace ); if (isTransliteration) { @@ -121,7 +122,7 @@ export const loadHTML = ( constant.TRANSLITERATION.toLowerCase(), textAlign, fontSize, - isNightMode, + theme, isLarivaar ); } @@ -133,7 +134,7 @@ export const loadHTML = ( constant.TRANSLATION.toLowerCase(), textAlign, fontSize, - isNightMode, + theme, isLarivaar ); } @@ -145,9 +146,10 @@ export const loadHTML = ( constant.TRANSLATION.toLowerCase(), textAlign, fontSize, - isNightMode, + theme, isLarivaar, - constant.GURMUKHI.toLowerCase() + constant.GURMUKHI.toLowerCase(), + constant.GURBANI_AKHAR_TRUE ); } @@ -158,7 +160,7 @@ export const loadHTML = ( constant.TRANSLATION.toLowerCase(), textAlign, fontSize, - isNightMode, + theme, isLarivaar ); } @@ -167,14 +169,7 @@ export const loadHTML = ( return contentHtml; }) .join(""); - const htmlContent = htmlTemplate( - backColor, - fileUri, - fontFace, - content, - isNightMode, - savePosition - ); + const htmlContent = htmlTemplate(backColor, fontFace, content, theme); return htmlContent; } catch (error) { logError(error); diff --git a/src/Settings/components/audio.jsx b/src/Settings/components/audio.jsx new file mode 100644 index 00000000..43a774ec --- /dev/null +++ b/src/Settings/components/audio.jsx @@ -0,0 +1,70 @@ +import React from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { ListItem, Icon, Switch } from "@rneui/themed"; +import { toggleAudio, toggleAudioAutoPlay, toggleAutoScroll } from "@common/actions"; +import useTheme from "@common/context"; +import useThemedStyles from "@common/hooks/useThemedStyles"; +import { STRINGS, ListItemTitle } from "@common"; +import createStyles from "../styles"; + +const Audio = () => { + const { theme } = useTheme(); + const styles = useThemedStyles(createStyles); + const isAudio = useSelector((state) => state.isAudio); + const isAudioAutoPlay = useSelector((state) => state.isAudioAutoPlay); + const isAutoScroll = useSelector((state) => state.isAutoScroll); + const dispatch = useDispatch(); + const { AUDIO, AUDIO_AUTO_PLAY } = STRINGS; + + // Audio settings configuration + const audioSettings = [ + { + id: "main", + title: AUDIO, + icon: "music-note", + value: isAudio, + action: toggleAudio, + showAlways: true, + }, + { + id: "autoPlay", + title: AUDIO_AUTO_PLAY, + icon: "play-circle-outline", + value: isAudioAutoPlay, + action: toggleAudioAutoPlay, + showAlways: false, + }, + ]; + + const renderAudioSetting = (setting) => { + const shouldShow = setting.showAlways || isAudio; + + if (!shouldShow) return null; + + return ( + + + + + + { + if (isAutoScroll) { + dispatch(toggleAutoScroll(false)); + } + dispatch(setting.action(value)); + }} + /> + + ); + }; + + return <>{audioSettings.map(renderAudioSetting)}; +}; + +export default Audio; diff --git a/src/Settings/components/autoScroll.jsx b/src/Settings/components/autoScroll.jsx index c6876438..37570a85 100644 --- a/src/Settings/components/autoScroll.jsx +++ b/src/Settings/components/autoScroll.jsx @@ -1,26 +1,25 @@ import React from "react"; -import { ListItem, Icon, Switch } from "@rneui/themed"; import { useSelector, useDispatch } from "react-redux"; -import { STRINGS } from "@common"; +import { ListItem, Icon, Switch } from "@rneui/themed"; import { toggleScreenAwake, toggleAutoScroll } from "@common/actions"; -import { iconNightColor, nightModeStyles, nightModeColor } from "../styles/nightModeStyles"; +import useTheme from "@common/context"; +import { STRINGS, ListItemTitle } from "@common"; const AutoScroll = () => { - const isNightMode = useSelector((state) => state.isNightMode); + const { theme } = useTheme(); const isAutoScroll = useSelector((state) => state.isAutoScroll); + const isAudio = useSelector((state) => state.isAudio); const dispatch = useDispatch(); - const iconColor = iconNightColor(isNightMode); - const { containerNightStyles } = nightModeStyles(isNightMode); - const nightColor = nightModeColor(isNightMode); const { AUTO_SCROLL } = STRINGS; return ( - - + + - {AUTO_SCROLL} + { /* The screen should remain active whenever Auto Scroll is enabled. */ dispatch(toggleScreenAwake(value)); diff --git a/src/Settings/components/collectStatistics.jsx b/src/Settings/components/collectStatistics.jsx index 4e22b6b2..a68e4ad2 100644 --- a/src/Settings/components/collectStatistics.jsx +++ b/src/Settings/components/collectStatistics.jsx @@ -1,23 +1,20 @@ import React from "react"; -import { ListItem, Avatar, Switch } from "@rneui/themed"; import { useDispatch, useSelector } from "react-redux"; -import { STRINGS, actions } from "@common"; -import { nightModeStyles, nightModeColor } from "../styles/nightModeStyles"; -import { styles } from "../styles"; +import { ListItem, Avatar, Switch } from "@rneui/themed"; +import { STRINGS, actions, useThemedStyles, ListItemTitle } from "@common"; +import createStyles from "../styles"; const CollectStatistics = () => { - const isNightMode = useSelector((state) => state.isNightMode); const isStatistics = useSelector((state) => state.isStatistics); - const { containerNightStyles } = nightModeStyles(isNightMode); - const nightColor = nightModeColor(isNightMode); + const styles = useThemedStyles(createStyles); const { COLLECT_STATISTICS } = STRINGS; const analyticsIcon = require("../../../images/analyticsicon.png"); const dispatch = useDispatch(); return ( - + - {COLLECT_STATISTICS} + { - const isNightMode = useSelector((state) => state.isNightMode); - const iconColor = useMemo(() => iconNightColor(isNightMode), [isNightMode]); - const { containerNightStyles } = useMemo(() => nightModeStyles(isNightMode), [isNightMode]); - const nightColor = useMemo(() => nightModeColor(isNightMode), [isNightMode]); - + const { theme } = useTheme(); + const styles = useThemedStyles(createStyles); return ( navigate(navigationTarget)} > - + - {title} + diff --git a/src/Settings/components/comon/bottomSheetComponent.jsx b/src/Settings/components/comon/bottomSheetComponent.jsx index b6087914..f0445fe9 100644 --- a/src/Settings/components/comon/bottomSheetComponent.jsx +++ b/src/Settings/components/comon/bottomSheetComponent.jsx @@ -1,13 +1,15 @@ import React, { useEffect, useState } from "react"; -import { View, Modal, Text, Dimensions, Pressable, Platform, StyleSheet } from "react-native"; -import { Divider, Icon, ListItem } from "@rneui/themed"; -import { BlurView } from "@react-native-community/blur"; -import { useDispatch, useSelector } from "react-redux"; -import { constant, colors } from "@common"; -import PropTypes from "prop-types"; +import { View, Modal, Dimensions, Pressable, Platform, StyleSheet } from "react-native"; import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context"; import SoundPlayer from "react-native-sound-player"; -import { styles, nightModeStyles, nightModeColor } from "../../styles"; +import { useDispatch } from "react-redux"; +import { BlurView } from "@react-native-community/blur"; +import { Divider, Icon, ListItem } from "@rneui/themed"; +import PropTypes from "prop-types"; +import useTheme from "@common/context"; +import useThemedStyles from "@common/hooks/useThemedStyles"; +import { constant, CustomText, ListItemTitle } from "@common"; +import createStyles from "../../styles"; const BottomSheetComponent = ({ isVisible, @@ -17,10 +19,9 @@ const BottomSheetComponent = ({ action, toggleVisible, }) => { + const { theme } = useTheme(); + const styles = useThemedStyles(createStyles); const dispatch = useDispatch(); - const isNightMode = useSelector((state) => state.isNightMode); - const { containerNightStyles, textNightStyle } = nightModeStyles(isNightMode); - const nightStyles = nightModeColor(isNightMode); const { width, height } = Dimensions.get("window"); const [orientation, setOrientation] = useState(width < height ? "PORTRAIT" : "LANDSCAPE"); @@ -58,21 +59,23 @@ const BottomSheetComponent = ({ > toggleVisible(false)}> - + {title} - + {actionConstant.map((item) => ( { toggleVisible(false); dispatch(action(item.key)); @@ -83,13 +86,13 @@ const BottomSheetComponent = ({ }} > - {item.title} + - {value === item.key && } + {value === item.key && } ))} {Platform.OS === "ios" && ( - + )} diff --git a/src/Settings/components/comon/index.jsx b/src/Settings/components/comon/index.jsx index 5ec0ac43..d2bb7794 100644 --- a/src/Settings/components/comon/index.jsx +++ b/src/Settings/components/comon/index.jsx @@ -1,4 +1,5 @@ -import ListItemComponent from "./listItemComponent"; import BottomSheetComponent from "./bottomSheetComponent"; +import ListItemComponent from "./listItemComponent"; +import ListItemWithIcon from "./ListitemWithIcon"; -export { ListItemComponent, BottomSheetComponent }; +export { ListItemComponent, BottomSheetComponent, ListItemWithIcon }; diff --git a/src/Settings/components/comon/listItemComponent.jsx b/src/Settings/components/comon/listItemComponent.jsx index 0cae7241..122e5252 100644 --- a/src/Settings/components/comon/listItemComponent.jsx +++ b/src/Settings/components/comon/listItemComponent.jsx @@ -1,27 +1,26 @@ -import React, { useMemo } from "react"; +import React from "react"; import { ListItem, Avatar, Icon } from "@rneui/themed"; import PropTypes from "prop-types"; -import { useSelector } from "react-redux"; -import { styles, nightModeStyles, iconNightColor } from "../../styles"; +import useTheme from "@common/context"; +import useThemedStyles from "@common/hooks/useThemedStyles"; +import { ListItemTitle } from "@common"; +import createStyles from "../../styles"; const ListItemComponent = ({ icon, title, value, isAvatar, actionConstant, onPressAction }) => { - const isNightMode = useSelector((state) => state.isNightMode); - const { containerNightStyles, textNightStyle, textNightGrey } = useMemo( - () => nightModeStyles(isNightMode), - [isNightMode] - ); - const iconColor = useMemo(() => iconNightColor(isNightMode), [isNightMode]); + const { theme } = useTheme(); + const styles = useThemedStyles(createStyles); return ( - + {isAvatar && } - {!isAvatar && } + {!isAvatar && } - {title} + {value && ( - - {actionConstant.filter((item) => item.key === value).map((item) => item.title)[0]} - + item.key === value).map((item) => item.title)[0]} + style={[styles.titleInfoStyle]} + /> )} diff --git a/src/Settings/components/comon/strings.js b/src/Settings/components/comon/strings.js index 84bb9753..8a46441d 100644 --- a/src/Settings/components/comon/strings.js +++ b/src/Settings/components/comon/strings.js @@ -9,10 +9,11 @@ export const getFontSizes = (strings) => [ ]; export const getFontFaces = (strings) => [ - { key: "AnmolLipiSG", title: strings.anmol_lipi }, - { key: "GurbaniAkharTrue", title: strings.gurbani_akhar_default }, - { key: "GurbaniAkharHeavyTrue", title: strings.gurbani_akhar_heavy }, - { key: "GurbaniAkharThickTrue", title: strings.gurbani_akhar_think }, + { key: constant.ANMOL_LIPI, title: strings.anmol_lipi }, + { key: constant.GURBANI_AKHAR_TRUE, title: strings.gurbani_akhar_default }, + { key: constant.GURBANI_AKHAR_HEAVY_TRUE, title: strings.gurbani_akhar_heavy }, + { key: constant.GURBANI_AKHAR_THICK_TRUE, title: strings.gurbani_akhar_think }, + { key: constant.BALOO_PAAJI, title: strings.baloo_paaji }, ]; export const getBaniLengths = (strings) => [ diff --git a/src/Settings/components/databaseUpdate.jsx b/src/Settings/components/databaseUpdate.jsx index 37203d7d..04fcec5c 100644 --- a/src/Settings/components/databaseUpdate.jsx +++ b/src/Settings/components/databaseUpdate.jsx @@ -1,17 +1,18 @@ -import PropTypes from "prop-types"; import React from "react"; -import { Image, Pressable, Text, View } from "react-native"; -import { STRINGS } from "@common"; -import { styles } from "../styles"; +import { Image, Pressable, View } from "react-native"; +import PropTypes from "prop-types"; +import { STRINGS, CustomText, useThemedStyles } from "@common"; +import createStyles from "../styles"; const baniDbLogo = require("../../../images/banidblogo.png"); const DatabaseUpdateBanner = ({ navigate }) => { + const styles = useThemedStyles(createStyles); return ( navigate("DatabaseUpdate")}> - {STRINGS.baniDBBannerText} + {STRINGS.baniDBBannerText} ); diff --git a/src/Settings/components/donate.jsx b/src/Settings/components/donate.jsx index 707bb9c6..613512c5 100644 --- a/src/Settings/components/donate.jsx +++ b/src/Settings/components/donate.jsx @@ -1,25 +1,22 @@ import React from "react"; import { Linking } from "react-native"; import { ListItem, Icon } from "@rneui/themed"; -import { useSelector } from "react-redux"; -import { STRINGS } from "@common"; -import { iconNightColor, nightModeStyles, nightModeColor } from "../styles/nightModeStyles"; +import { STRINGS, useTheme, useThemedStyles, ListItemTitle } from "@common"; +import createStyles from "../styles"; const Donate = () => { - const isNightMode = useSelector((state) => state.isNightMode); - const iconColor = iconNightColor(isNightMode); - const { containerNightStyles } = nightModeStyles(isNightMode); - const nightColor = nightModeColor(isNightMode); + const { theme } = useTheme(); + const styles = useThemedStyles(createStyles); const { donate } = STRINGS; return ( Linking.openURL("https://khalisfoundation.org/donate/")} > - + - {donate} + diff --git a/src/Settings/components/editBaniOrder.jsx b/src/Settings/components/editBaniOrder.jsx index 76f87190..15600801 100644 --- a/src/Settings/components/editBaniOrder.jsx +++ b/src/Settings/components/editBaniOrder.jsx @@ -1,24 +1,22 @@ import React from "react"; import { ListItem, Avatar } from "@rneui/themed"; import PropTypes from "prop-types"; -import { STRINGS } from "@common"; -import { nightModeStyles, nightModeColor } from "../styles/nightModeStyles"; -import { styles } from "../styles"; +import { STRINGS, useThemedStyles, ListItemTitle } from "@common"; +import createStyles from "../styles"; -const EditBaniOrder = ({ navigate, isNightMode }) => { - const { containerNightStyles } = nightModeStyles(isNightMode); - const nightColor = nightModeColor(isNightMode); +const EditBaniOrder = ({ navigate }) => { + const styles = useThemedStyles(createStyles); const { EDIT_BANI_ORDER } = STRINGS; const rearrangeIcon = require("../../../images/rearrangeicon.png"); return ( navigate("EditBaniOrder")} > - {EDIT_BANI_ORDER} + @@ -26,6 +24,5 @@ const EditBaniOrder = ({ navigate, isNightMode }) => { }; EditBaniOrder.propTypes = { navigate: PropTypes.func.isRequired, - isNightMode: PropTypes.bool.isRequired, }; export default EditBaniOrder; diff --git a/src/Settings/components/keepAwake.jsx b/src/Settings/components/keepAwake.jsx index 8d05c6eb..44078d8e 100644 --- a/src/Settings/components/keepAwake.jsx +++ b/src/Settings/components/keepAwake.jsx @@ -1,26 +1,23 @@ import React from "react"; -import { ListItem, Switch, Avatar } from "@rneui/themed"; import { useSelector, useDispatch } from "react-redux"; -import { STRINGS } from "@common"; +import { ListItem, Switch, Avatar } from "@rneui/themed"; import { toggleScreenAwake } from "@common/actions"; -import { nightModeStyles, nightModeColor } from "../styles/nightModeStyles"; -import { styles } from "../styles"; +import { STRINGS, useThemedStyles, ListItemTitle } from "@common"; +import createStyles from "../styles"; const KeepAwake = () => { + const styles = useThemedStyles(createStyles); const dispatch = useDispatch(); - const isNightMode = useSelector((state) => state.isNightMode); const isScreenAwake = useSelector((state) => state.isScreenAwake); const isAutoScroll = useSelector((state) => state.isAutoScroll); - const { containerNightStyles } = nightModeStyles(isNightMode); - const nightColor = nightModeColor(isNightMode); const { KEEP_AWAKE } = STRINGS; const screenIcon = require("../../../images/screenonicon.png"); return ( - + - {KEEP_AWAKE} + { - const isNightMode = useSelector((state) => state.isNightMode); + const { theme } = useTheme(); + const styles = useThemedStyles(createStyles); const isLarivaar = useSelector((state) => state.isLarivaar); const isLarivaarAssist = useSelector((state) => state.isLarivaarAssist); const dispatch = useDispatch(); - const { containerNightStyles, textNightStyle } = nightModeStyles(isNightMode); - const iconColor = iconNightColor(isNightMode); const larivaarIcon = require("../../../images/larivaaricon.png"); return ( <> - + - {STRINGS.larivaar} + dispatch(toggleLarivaar(value))} /> {isLarivaar && ( - - + + - {STRINGS.larivaar_assist} + { + const { theme } = useTheme(); + const styles = useThemedStyles(createStyles); const isParagraphMode = useSelector((state) => state.isParagraphMode); - const isNightMode = useSelector((state) => state.isNightMode); const dispatch = useDispatch(); - const iconColor = iconNightColor(isNightMode); - const { containerNightStyles } = nightModeStyles(isNightMode); - const nightColor = nightModeColor(isNightMode); const { PARAGRAPH_MODE } = STRINGS; return ( - - + + - {PARAGRAPH_MODE} + { - const isNightMode = useSelector((state) => state.isNightMode); + const { theme } = useTheme(); + const styles = useThemedStyles(createStyles); const [isLabelModal, toggleLabelModal] = useState(false); const reminderBanis = useSelector((state) => state.reminderBanis); const dispatch = useDispatch(); const { key, title } = section; - let backColor; - if (isActive) { - backColor = isNightMode ? colors.ACTIVE_VIEW_COLOR_NIGHT_MODE : colors.ACTIVE_VIEW_COLOR; - } else { - backColor = isNightMode ? colors.INACTIVE_VIEW_COLOR_NIGHT_MODE : colors.INACTIVE_VIEW_COLOR; - } + + const backColor = isActive ? theme.colors.activeView : theme.colors.inactiveView; + const hideModal = () => { toggleLabelModal(false); }; @@ -38,19 +38,8 @@ const AccordianContent = ({ section, isActive }) => { }} > - - - {title} - + + {title} @@ -60,21 +49,11 @@ const AccordianContent = ({ section, isActive }) => { }} > - - - {STRINGS.delete} - + + {STRINGS.delete} - + {isLabelModal && } ); diff --git a/src/Settings/components/reminders/ReminderOptions/components/AccordianHeader.jsx b/src/Settings/components/reminders/ReminderOptions/components/AccordianHeader.jsx index 807ed816..9daeab1e 100644 --- a/src/Settings/components/reminders/ReminderOptions/components/AccordianHeader.jsx +++ b/src/Settings/components/reminders/ReminderOptions/components/AccordianHeader.jsx @@ -1,17 +1,20 @@ import React, { useState } from "react"; -import { View, TouchableOpacity, Text } from "react-native"; -import { Switch, Icon, Divider } from "@rneui/themed"; -import PropTypes from "prop-types"; -import { useDispatch, useSelector } from "react-redux"; +import { View, TouchableOpacity } from "react-native"; import DateTimePicker from "react-native-modal-datetime-picker"; +import { useDispatch, useSelector } from "react-redux"; +import { Switch, Icon, Divider } from "@rneui/themed"; import moment from "moment"; -import { colors, constant, updateReminders, trackReminderEvent } from "@common"; +import PropTypes from "prop-types"; import { setReminderBanis } from "@common/actions"; -import { styles } from "../styles"; +import useTheme from "@common/context"; +import useThemedStyles from "@common/hooks/useThemedStyles"; +import { constant, updateReminders, trackReminderEvent, CustomText } from "@common"; +import createStyles from "../styles"; const AccordianHeader = ({ section, isActive }) => { + const { theme } = useTheme(); + const styles = useThemedStyles(createStyles); const reminderBanis = useSelector((state) => state.reminderBanis); - const isNightMode = useSelector((state) => state.isNightMode); const isTransliteration = useSelector((state) => state.isTransliteration); const isReminders = useSelector((state) => state.isReminders); const reminderSound = useSelector((state) => state.reminderSound); @@ -54,39 +57,37 @@ const AccordianHeader = ({ section, isActive }) => { - {isTransliteration ? translit : gurmukhi} - + handelSwitchToggled(value, key)} /> toggleTimePicker(true)}> - {time} - + - + { + const { theme } = useTheme(); const dispatch = useDispatch(); const isReminders = useSelector((state) => state.isReminders); const reminderSound = useSelector((state) => state.reminderSound); @@ -13,7 +15,7 @@ const useHeader = (baniListData, navigation, selector) => { name="arrow-back" size={30} onPress={() => navigation.goBack()} - color={colors.WHITE_COLOR} + color={theme.staticColors.WHITE_COLOR} /> ); const headerRight = () => { @@ -21,7 +23,7 @@ const useHeader = (baniListData, navigation, selector) => { <> { @@ -30,7 +32,7 @@ const useHeader = (baniListData, navigation, selector) => { /> { selector.current.open(); @@ -43,12 +45,12 @@ const useHeader = (baniListData, navigation, selector) => { navigation.setOptions({ title: STRINGS.set_reminder_options, headerTitleStyle: { - color: colors.WHITE_COLOR, + color: theme.staticColors.WHITE_COLOR, fontWeight: "normal", fontSize: 18, }, headerStyle: { - backgroundColor: colors.TOOLBAR_COLOR_ALT2, + backgroundColor: theme.colors.headerVariant, }, headerLeft, headerRight: () => (baniListData.length > 0 ? headerRight() : null), diff --git a/src/Settings/components/reminders/ReminderOptions/index.js b/src/Settings/components/reminders/ReminderOptions/index.js index 71480cc5..a97d74af 100644 --- a/src/Settings/components/reminders/ReminderOptions/index.js +++ b/src/Settings/components/reminders/ReminderOptions/index.js @@ -1,13 +1,14 @@ import React, { useState, useRef, useMemo } from "react"; -import { useDispatch, useSelector } from "react-redux"; import { View, ScrollView } from "react-native"; -import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context"; -import Accordion from "react-native-collapsible/Accordion"; -import PropTypes from "prop-types"; import ModalSelector from "react-native-modal-selector"; +import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context"; +import { useDispatch, useSelector } from "react-redux"; import moment from "moment"; +import PropTypes from "prop-types"; +import Accordion from "react-native-collapsible/Accordion"; +import useTheme from "@common/context"; +import useThemedStyles from "@common/hooks/useThemedStyles"; import { - colors, constant, STRINGS, actions, @@ -16,13 +17,14 @@ import { logMessage, StatusBarComponent, } from "@common"; -import { styles, accordianNightColor, optionContainer } from "./styles"; import { AccordianContent, AccordianHeader } from "./components"; import { useHeader, useFetchBani } from "./hooks"; +import createStyles from "./styles"; const ReminderOptions = ({ navigation }) => { logMessage(constant.REMINDER_OPTIONS); - const isNightMode = useSelector((state) => state.isNightMode); + const { theme } = useTheme(); + const styles = useThemedStyles(createStyles); const reminderBanis = useSelector((state) => state.reminderBanis); const isReminders = useSelector((state) => state.isReminders); const reminderSound = useSelector((state) => state.reminderSound); @@ -37,9 +39,6 @@ const ReminderOptions = ({ navigation }) => { const dispatch = useDispatch(); const selector = useRef(null); - const { backgroundColor, color } = optionContainer(isNightMode); - - const accNightColor = useMemo(() => accordianNightColor(isNightMode), [isNightMode]); useFetchBani(setBaniListData, setReminderBaniData, setStateData, parsedReminderBanis); useHeader(baniListData, navigation, selector); @@ -73,20 +72,15 @@ const ReminderOptions = ({ navigation }) => { return ( - + - + {stateData.length > 0 && ( ( )} @@ -105,15 +99,19 @@ const ReminderOptions = ({ navigation }) => { ]} data={reminderBaniData} cancelText={STRINGS.cancel} - optionTextStyle={{ ...styles.modalSelectText, color, ...fontFamily }} + optionTextStyle={{ + ...styles.modalSelectText, + color: theme.colors.primaryText, + ...fontFamily, + }} onChange={(option) => { createReminder(option); }} customSelector={} ref={selector} - cancelTextStyle={{ color }} - cancelStyle={{ backgroundColor }} - optionContainerStyle={{ backgroundColor }} + cancelTextStyle={{ color: theme.colors.primaryText }} + cancelStyle={{ backgroundColor: theme.colors.surfaceGrey }} + optionContainerStyle={{ backgroundColor: theme.colors.surfaceGrey }} /> diff --git a/src/Settings/components/reminders/ReminderOptions/modals/LabelModal.jsx b/src/Settings/components/reminders/ReminderOptions/modals/LabelModal.jsx index e56d5cb4..592aff74 100644 --- a/src/Settings/components/reminders/ReminderOptions/modals/LabelModal.jsx +++ b/src/Settings/components/reminders/ReminderOptions/modals/LabelModal.jsx @@ -1,19 +1,19 @@ import React, { useState } from "react"; -import { Modal, Text, TextInput, View, TouchableOpacity } from "react-native"; +import { Modal, TextInput, View, TouchableOpacity } from "react-native"; import { useDispatch, useSelector } from "react-redux"; import PropTypes from "prop-types"; import { setReminderBanis } from "@common/actions"; -import { updateReminders, colors, STRINGS } from "@common"; -import { styles } from "../styles"; +import { updateReminders, STRINGS, CustomText, useThemedStyles, useTheme } from "@common"; +import createStyles from "../styles"; const LabelModal = ({ section, onHide }) => { + const styles = useThemedStyles(createStyles); const { title } = section; const [reminderTitle, setReminderTitle] = useState(title); - const isNightMode = useSelector((state) => state.isNightMode); const reminderBanis = useSelector((state) => state.reminderBanis); const isReminders = useSelector((state) => state.isReminders); const reminderSound = useSelector((state) => state.reminderSound); - + const { theme } = useTheme(); const dispatch = useDispatch(); const confirmReminderLabel = () => { @@ -29,15 +29,12 @@ const LabelModal = ({ section, onHide }) => { - {STRINGS.notification_text} + {STRINGS.notification_text} setReminderTitle(label)} - selectionColor={colors.MODAL_ACCENT_NIGHT_MODE} + selectionColor={theme.colors.underlayColor} /> @@ -47,7 +44,7 @@ const LabelModal = ({ section, onHide }) => { }} style={{ marginRight: 30 }} > - {STRINGS.cancel} + {STRINGS.cancel} { @@ -55,7 +52,7 @@ const LabelModal = ({ section, onHide }) => { onHide(); }} > - {STRINGS.ok} + {STRINGS.ok} diff --git a/src/Settings/components/reminders/ReminderOptions/styles.js b/src/Settings/components/reminders/ReminderOptions/styles.js index c8614212..804574fc 100644 --- a/src/Settings/components/reminders/ReminderOptions/styles.js +++ b/src/Settings/components/reminders/ReminderOptions/styles.js @@ -1,42 +1,59 @@ -import { StyleSheet } from "react-native"; -import { colors } from "@common"; - -export const styles = StyleSheet.create({ +const createStyles = (theme) => ({ viewColumn: { flexDirection: "column" }, viewRow: { flexDirection: "row", justifyContent: "space-between" }, - cardTitle: { fontSize: 24 }, - flexView: { flex: 1 }, - timeFont: { fontSize: 44 }, - accContentText: { fontSize: 14 }, - accContentWrapper: { flexDirection: "row", alignItems: "center", margin: 5 }, - modalSelectText: { fontSize: 28 }, + cardTitle: { + fontSize: theme.typography.sizes.xxxl, + color: theme.colors.primaryText, + fontWeight: theme.typography.weights.medium, + }, + flexView: { flex: 1, backgroundColor: theme.colors.inactiveView }, + timeFont: { + fontSize: theme.typography.sizes.huge + theme.spacing.lg, + color: theme.colors.primaryText, + fontWeight: theme.typography.weights.light, + }, + accContentText: { + fontSize: theme.typography.sizes.md, + color: theme.colors.componentColor, + }, + accContentWrapper: { + flexDirection: "row", + alignItems: "center", + margin: theme.spacing.sm, + }, + modalSelectText: { + fontSize: theme.typography.sizes.huge, + fontWeight: theme.typography.weights.medium, + color: theme.colors.primaryText, + }, textInput: { - height: 40, - borderRadius: 5, - borderColor: colors.MODAL_ACCENT_NIGHT_MODE_ALT, + height: theme.components.input.minHeight - theme.spacing.sm, + borderRadius: theme.components.input.borderRadius, + borderColor: theme.colors.underlayColor, borderWidth: 1, - padding: 5, + padding: theme.components.input.paddingHorizontal, + color: theme.colors.primaryText, + fontSize: theme.typography.sizes.lg, }, labelModalWrapper: { flex: 1, justifyContent: "center", alignItems: "center" }, - labelViewWrapper: { backgroundColor: colors.WHITE_COLOR, padding: 20, width: 300 }, - labelText: { paddingBottom: 5, color: colors.MODAL_ACCENT_NIGHT_MODE }, + labelViewWrapper: { + backgroundColor: theme.staticColors.WHITE_COLOR, + padding: theme.spacing.xl, + width: 300, + borderRadius: theme.radius.lg, + }, + labelText: { + paddingBottom: theme.spacing.sm, + color: theme.colors.underlayColor, + fontSize: theme.typography.sizes.lg, + fontWeight: theme.typography.weights.medium, + }, labelButtonWrapper: { flexDirection: "row", justifyContent: "flex-end", alignItems: "center", - padding: 10, + padding: theme.spacing.md, }, - modalBackColor: { backgroundColor: colors.NIGHT_GREY_COLOR }, + modalBackColor: { backgroundColor: theme.colors.surfaceGrey }, }); - -export const accordianNightColor = (isNightMode) => { - const color = isNightMode ? colors.NIGHT_BLACK : colors.WHITE_COLOR; - return color; -}; - -export const optionContainer = (isNightMode) => { - return { - backgroundColor: isNightMode ? colors.NIGHT_GREY_COLOR : colors.WHITE_COLOR, - color: isNightMode ? colors.WHITE_COLOR : colors.NIGHT_GREY_COLOR, - }; -}; +export default createStyles; diff --git a/src/Settings/components/reminders/ReminderOptions/utils/index.js b/src/Settings/components/reminders/ReminderOptions/utils/index.js index e5a737b9..b527cce5 100644 --- a/src/Settings/components/reminders/ReminderOptions/utils/index.js +++ b/src/Settings/components/reminders/ReminderOptions/utils/index.js @@ -1,5 +1,5 @@ -import { updateReminders, constant, trackReminderEvent, STRINGS } from "@common"; import { setReminderBanis } from "@common/actions"; +import { updateReminders, constant, trackReminderEvent, STRINGS } from "@common"; const setDefaultReminders = async (baniListData, dispatch, isReminders, reminderSound) => { const baniList = baniListData; diff --git a/src/Settings/components/reminders/reminders.jsx b/src/Settings/components/reminders/reminders.jsx index ae193334..60f2c9db 100644 --- a/src/Settings/components/reminders/reminders.jsx +++ b/src/Settings/components/reminders/reminders.jsx @@ -1,8 +1,10 @@ import React, { useState } from "react"; import { Alert, Linking } from "react-native"; -import { ListItem, Icon, Switch } from "@rneui/themed"; import { useSelector, useDispatch } from "react-redux"; +import { ListItem, Icon, Switch } from "@rneui/themed"; import PropTypes from "prop-types"; +import useTheme from "@common/context"; +import useThemedStyles from "@common/hooks/useThemedStyles"; import { STRINGS, cancelAllReminders, @@ -11,16 +13,18 @@ import { logError, logMessage, FallBack, + ListItemTitle, } from "@common"; import { getBaniList } from "@database"; -import { nightModeStyles, iconNightColor } from "../../styles"; +import createStyles from "../../styles"; import { ListItemComponent, BottomSheetComponent } from "../comon"; -import setDefaultReminders from "./ReminderOptions/utils"; import { getReminderSound } from "../comon/strings"; +import setDefaultReminders from "./ReminderOptions/utils"; const RemindersComponent = ({ navigation }) => { + const { theme } = useTheme(); + const styles = useThemedStyles(createStyles); const REMINDER_SOUNDS = getReminderSound(STRINGS); - const isNightMode = useSelector((state) => state.isNightMode); const isReminders = useSelector((state) => state.isReminders); const reminderSound = useSelector((state) => state.reminderSound); const transliterationLanguage = useSelector((state) => state.transliterationLanguage); @@ -28,8 +32,6 @@ const RemindersComponent = ({ navigation }) => { const dispatch = useDispatch(); const { navigate } = navigation; - const { containerNightStyles, textNightStyle } = nightModeStyles(isNightMode); - const iconColor = iconNightColor(isNightMode); const redirectToSettings = async () => { Alert.alert(STRINGS.permissionTitle, STRINGS.premissionDescription, [ @@ -76,10 +78,10 @@ const RemindersComponent = ({ navigation }) => { return ( <> - - + + - {STRINGS.reminders} + handleReminders(value)} /> @@ -87,12 +89,12 @@ const RemindersComponent = ({ navigation }) => { {isReminders && ( navigate("ReminderOptions")} > - + - {STRINGS.set_reminder_options} + diff --git a/src/Settings/components/statusBar.jsx b/src/Settings/components/statusBar.jsx index aa7a6680..27e85079 100644 --- a/src/Settings/components/statusBar.jsx +++ b/src/Settings/components/statusBar.jsx @@ -1,26 +1,27 @@ import React from "react"; -import { ListItem, Icon, Switch } from "@rneui/themed"; import { useSelector, useDispatch } from "react-redux"; +import { ListItem, Icon, Switch } from "@rneui/themed"; import { toggleStatusBar } from "@common/actions"; -import STRINGS from "@common/localization"; -import { iconNightColor, nightModeStyles, nightModeColor } from "../styles/nightModeStyles"; +import useTheme from "@common/context"; +import useThemedStyles from "@common/hooks/useThemedStyles"; +import { STRINGS, ListItemTitle } from "@common"; +import createStyles from "../styles"; const StatusBar = () => { const isStatusBar = useSelector((state) => state.isStatusBar); - const isNightMode = useSelector((state) => state.isNightMode); - + const { theme } = useTheme(); + const styles = useThemedStyles(createStyles); const dispatch = useDispatch(); - const iconColor = iconNightColor(isNightMode); - const { containerNightStyles } = nightModeStyles(isNightMode); - const nightColor = nightModeColor(isNightMode); const { HIDE_STATUS_BAR } = STRINGS; return ( - - {!isStatusBar && } - {isStatusBar && } + + {!isStatusBar && ( + + )} + {isStatusBar && } - {HIDE_STATUS_BAR} + dispatch(toggleStatusBar(value))} /> diff --git a/src/Settings/components/theme.jsx b/src/Settings/components/theme.jsx index 6e1fbb0c..1641dc5e 100644 --- a/src/Settings/components/theme.jsx +++ b/src/Settings/components/theme.jsx @@ -1,8 +1,7 @@ -import React, { useState, useEffect } from "react"; -import { Appearance } from "react-native"; -import { useDispatch, useSelector } from "react-redux"; -import { setTheme, toggleNightMode } from "@common/actions"; -import { constant, STRINGS } from "@common"; +import React, { useState } from "react"; +import { useSelector } from "react-redux"; +import { setTheme } from "@common/actions"; +import { STRINGS } from "@common"; import { BottomSheetComponent, ListItemComponent } from "./comon"; import { getTheme } from "./comon/strings"; @@ -10,24 +9,8 @@ const ThemeComponent = () => { const [isVisible, toggleVisible] = useState(false); const theme = useSelector((state) => state.theme); const themeIcon = require("../../../images/bgcoloricon.png"); - const dispatch = useDispatch(); const THEMES = getTheme(STRINGS); - useEffect(() => { - const colorScheme = Appearance.getColorScheme(); - - switch (theme) { - case constant.Light: - dispatch(toggleNightMode(false)); - break; - case constant.Dark: - dispatch(toggleNightMode(true)); - break; - default: - dispatch(toggleNightMode(colorScheme !== constant.Light.toLowerCase())); - } - }, [theme]); - return ( <> { + const { theme } = useTheme(); + const styles = useThemedStyles(createStyles); const translationAvatar = require("../../../images/englishicon.png"); const isEnglishTranslation = useSelector((state) => state.isEnglishTranslation); const isSpanishTranslation = useSelector((state) => state.isSpanishTranslation); const isPunjabiTranslation = useSelector((state) => state.isPunjabiTranslation); - const isNightMode = useSelector((state) => state.isNightMode); const dispatch = useDispatch(); const [isExpanded, toggleIsExpanded] = useState(false); - const nightColor = iconNightColor(isNightMode); return ( toggleIsExpanded(!isExpanded)} content={ <> - - {STRINGS.translations} - + } icon={{ name: "chevron-down", type: "material-community", - color: nightColor, + color: theme.colors.primaryText, size: 26, }} > - + - - {STRINGS.en_translations} - + { /> - + - - {STRINGS.pu_translations} - + { /> - + - - {STRINGS.es_translations} - + { + const styles = useThemedStyles(createStyles); const romanizedIcon = require("../../../images/romanizeicon.png"); const [isVisible, toggleVisible] = useState(false); const transliterationLanguage = useSelector((state) => state.transliterationLanguage); const isTransliteration = useSelector((state) => state.isTransliteration); - const isNightMode = useSelector((state) => state.isNightMode); const TRANSLITERATION_LANGUAGES = getTransliteration(STRINGS); const dispatch = useDispatch(); - // Set default transliteration to English - useEffect(() => { - if (!isTransliteration) { - dispatch(setTransliteration(constant.ENGLISH)); - } - }, [isTransliteration]); - return ( <> - + - - {STRINGS.transliteration} - + { + const { theme } = useTheme(); + const styles = useThemedStyles(createStyles); const [isVishraamOptionVisible, toggleVishraamOptionVisible] = useState(false); const [isVishraamSourceVisible, toggleVishraamSourceVisible] = useState(false); const isVishraam = useSelector((state) => state.isVishraam); const vishraamOption = useSelector((state) => state.vishraamOption); const vishraamSource = useSelector((state) => state.vishraamSource); - const isNightMode = useSelector((state) => state.isNightMode); const dispatch = useDispatch(); - const { containerNightStyles, textNightStyle } = nightModeStyles(isNightMode); - const iconColor = iconNightColor(isNightMode); const VISHRAAM_OPTIONS = getVishraamOption(STRINGS); const VISHRAAM_SOURCES = getVishraamSource(STRINGS); return ( <> - - + + - {STRINGS.show_vishraams} + dispatch(toggleVishraam(value))} /> diff --git a/src/Settings/hooks/useHeader.js b/src/Settings/hooks/useHeader.js new file mode 100644 index 00000000..faab8a42 --- /dev/null +++ b/src/Settings/hooks/useHeader.js @@ -0,0 +1,21 @@ +import React, { useEffect } from "react"; +import { BackIconComponent } from "@common/components"; +import useTheme from "@common/context"; +import useThemedStyles from "@common/hooks/useThemedStyles"; +import { STRINGS } from "@common"; +import createStyles from "../styles"; + +const useHeader = (navigation) => { + const { theme } = useTheme(); + const { headerTitleStyle, headerStyle } = useThemedStyles(createStyles); + const headerLeft = () => ; + useEffect(() => { + navigation.setOptions({ + title: STRINGS.Settings, + headerTitleStyle, + headerStyle, + headerLeft, + }); + }, [theme]); +}; +export default useHeader; diff --git a/src/Settings/index.js b/src/Settings/index.js index aadb9cd1..ef0df163 100644 --- a/src/Settings/index.js +++ b/src/Settings/index.js @@ -1,47 +1,49 @@ import React, { useEffect } from "react"; +import { StatusBar, ScrollView } from "react-native"; import { useSelector } from "react-redux"; import PropTypes from "prop-types"; -import { StatusBar, ScrollView, Text } from "react-native"; +import useTheme from "@common/context"; +import useThemedStyles from "@common/hooks/useThemedStyles"; import { STRINGS, - colors, - useScreenAnalytics, - constant, - logMessage, StatusBarComponent, SafeArea, + CustomText, + BottomNavigation, + useBackHandler, } from "@common"; -import { nightModeStyles } from "./styles/nightModeStyles"; -import FontSizeComponent from "./components/fontSize"; +import Audio from "./components/audio"; +import AutoScroll from "./components/autoScroll"; +import BaniLengthComponent from "./components/baniLength"; +import CollectStatistics from "./components/collectStatistics"; +import ListItemWithIcon from "./components/comon/ListitemWithIcon"; +import DatabaseUpdateBanner from "./components/databaseUpdate"; +import Donate from "./components/donate"; +import EditBaniOrder from "./components/editBaniOrder"; import FontFaceComponent from "./components/fontFace"; +import FontSizeComponent from "./components/fontSize"; +import KeepAwake from "./components/keepAwake"; import LanguageComponent from "./components/language"; -import TransliterationComponent from "./components/transliteration"; -import ThemeComponent from "./components/theme"; -import HideStatusBar from "./components/statusBar"; -import BaniLengthComponent from "./components/baniLength"; import LarivaarComponent from "./components/larivaar"; import PadchedSettingsComponent from "./components/padched"; -import VishraamComponent from "./components/vishraam"; -import TranslationComponent from "./components/translation"; -import RemindersComponent from "./components/reminders/reminders"; -import AutoScroll from "./components/autoScroll"; -import KeepAwake from "./components/keepAwake"; -import EditBaniOrder from "./components/editBaniOrder"; import ParagraphMode from "./components/paragraphMode"; -import CollectStatistics from "./components/collectStatistics"; -import Donate from "./components/donate"; -import styles from "./styles/styles"; -import ListItemWithIcon from "./components/comon/ListitemWithIcon"; -import DatabaseUpdateBanner from "./components/databaseUpdate"; +import RemindersComponent from "./components/reminders/reminders"; +import HideStatusBar from "./components/statusBar"; +import ThemeComponent from "./components/theme"; +import TranslationComponent from "./components/translation"; +import TransliterationComponent from "./components/transliteration"; +import VishraamComponent from "./components/vishraam"; +import useHeader from "./hooks/useHeader"; +import createStyles from "./styles"; const Settings = ({ navigation }) => { - logMessage(constant.SETTINGS); - useScreenAnalytics(constant.SETTINGS); - const isNightMode = useSelector((state) => state.isNightMode); + useHeader(navigation); + useBackHandler(); const isDatabaseUpdateAvailable = useSelector((state) => state.isDatabaseUpdateAvailable); const { navigate } = navigation; - const { scrollViewNightStyles, backgroundNightStyle } = nightModeStyles(isNightMode); + const { theme } = useTheme(); + const styles = useThemedStyles(createStyles); const { displayOptionsText, end } = styles; const { DISPLAY_OPTIONS, BANI_OPTIONS, OTHER_OPTIONS } = STRINGS; const language = useSelector((state) => state.language); @@ -49,20 +51,17 @@ const Settings = ({ navigation }) => { useEffect(() => { navigation.setOptions({ title: STRINGS.settings, + headerTitleStyle: styles.headerTitleStyle, }); }, [language]); return ( - - + + {isDatabaseUpdateAvailable && } - {DISPLAY_OPTIONS} + {DISPLAY_OPTIONS} @@ -73,16 +72,17 @@ const Settings = ({ navigation }) => { + + ); }; diff --git a/src/Settings/styles/index.js b/src/Settings/styles/index.js index 27469734..f7dd14ca 100644 --- a/src/Settings/styles/index.js +++ b/src/Settings/styles/index.js @@ -1,4 +1,94 @@ -import styles from "./styles"; -import { iconNightColor, nightModeStyles, nightModeColor } from "./nightModeStyles"; - -export { styles, iconNightColor, nightModeColor, nightModeStyles }; +const createStyles = (theme) => ({ + headerTitleStyle: { + color: theme.colors.primaryText, + fontFamily: theme.typography.fonts.balooPaajiSemiBold, + }, + headerStyle: { + backgroundColor: theme.colors.surface, + }, + nightBackColor: { backgroundColor: theme.staticColors.NIGHT_BLACK }, + iconStyle: { alignSelf: "flex-start" }, + imageStyle: {}, + settingText: { + fontSize: theme.typography.sizes.xl, + alignSelf: "center", + color: theme.colors.primaryText, + position: "absolute", + top: theme.spacing.xl, + fontFamily: theme.typography.fonts.balooPaaji, + }, + settingsView: { backgroundColor: theme.colors.surface }, + displayOptionsText: { + padding: theme.spacing.sm + theme.spacing.xs, + backgroundColor: theme.colors.surface, + color: theme.colors.primaryText, + fontSize: theme.typography.sizes.md, + lineHeight: theme.typography.sizes.md * theme.typography.lineHeights.normal, + borderTopWidth: 1, + borderTopColor: theme.colors.separator, + }, + bottomSheetTitle: { + textAlign: "center", + fontSize: theme.typography.sizes.xxl, + padding: theme.spacing.xl, + borderTopLeftRadius: theme.radius.lg + theme.spacing.sm, + borderTopRightRadius: theme.radius.lg + theme.spacing.sm, + fontWeight: theme.typography.weights.medium, + }, + titleInfoStyle: { + fontSize: theme.typography.sizes.sm, + color: theme.colors.textDisabled, + }, + end: { + padding: theme.spacing.xxl + theme.spacing.md, + backgroundColor: theme.colors.surface, + }, + avatarStyle: { width: "100%", height: "100%", resizeMode: "contain" }, + viewWrapper: { + justifyContent: "center", + marginTop: "auto", + marginLeft: "auto", + marginRight: "auto", + bottom: 0, + borderTopLeftRadius: theme.radius.lg + theme.spacing.sm, + borderTopRightRadius: theme.radius.lg + theme.spacing.sm, + overflow: "hidden", + }, + width_100: { + width: "98%", + }, + width_90: { + width: "70%", + }, + blurViewStyle: { position: "absolute", top: 0, bottom: 0, left: 0, right: 0 }, + androidViewWrapper: { + flex: 1, + justifyContent: "flex-end", + backgroundColor: "transparent", + width: "100%", + }, + databaseUpdateBannerWrapper: { + flex: 1, + flexDirection: "row", + backgroundColor: theme.colors.primary, + justifyContent: "center", + alignItems: "center", + padding: theme.spacing.sm, + }, + baniDbImage: { + width: theme.spacing.xl, + height: theme.spacing.xl, + marginRight: theme.spacing.md, + }, + updateText: { + color: theme.staticColors.WHITE_COLOR, + fontSize: theme.typography.sizes.md, + }, + listItemTitle: { + color: theme.colors.primaryText, + fontSize: theme.typography.sizes.lg, + lineHeight: theme.typography.sizes.lg * theme.typography.lineHeights.normal, + }, + containerNightStyles: { backgroundColor: theme.colors.surfaceGrey }, +}); +export default createStyles; diff --git a/src/Settings/styles/nightModeStyles.js b/src/Settings/styles/nightModeStyles.js deleted file mode 100644 index df1dd7ec..00000000 --- a/src/Settings/styles/nightModeStyles.js +++ /dev/null @@ -1,29 +0,0 @@ -import colors from "@common/colors"; - -export const nightModeStyles = (isNightMode) => ({ - scrollViewNightStyles: { - backgroundColor: isNightMode ? colors.NIGHT_BLACK : colors.LABEL_COLORS, - color: isNightMode ? colors.WHITE_COLOR : colors.NIGHT_BLACK, - }, - containerNightStyles: { - backgroundColor: isNightMode ? colors.NIGHT_GREY_COLOR : colors.WHITE_COLOR, - }, - backgroundNightStyle: { - backgroundColor: isNightMode ? colors.NIGHT_BLACK : colors.WHITE_COLOR, - }, - textNightStyle: { - color: isNightMode ? colors.WHITE_COLOR : colors.NIGHT_BLACK, - }, - textNightGrey: { - color: isNightMode ? colors.WHITE_COLOR : colors.DISABLED_TEXT_COLOR, - }, -}); - -export const iconNightColor = (isNightMode) => { - const color = isNightMode ? colors.COMPONENT_COLOR_NIGHT_MODE : colors.COMPONENT_COLOR; - return color; -}; - -export const nightModeColor = (isNightMode) => ({ - color: isNightMode ? colors.WHITE_COLOR : colors.NIGHT_BLACK, -}); diff --git a/src/Settings/styles/styles.js b/src/Settings/styles/styles.js deleted file mode 100644 index 71a0de9b..00000000 --- a/src/Settings/styles/styles.js +++ /dev/null @@ -1,64 +0,0 @@ -import { StyleSheet } from "react-native"; -import { colors } from "@common"; - -const styles = StyleSheet.create({ - nightBackColor: { backgdroundColor: colors.NIGHT_BLACK }, - iconStyle: { alignSelf: "flex-start" }, - imageStyle: {}, - headerView: { backgroundColor: colors.TOOLBAR_COLOR_ALT, padding: 15 }, - settingText: { - fontSize: 18, - alignSelf: "center", - color: colors.TOOLBAR_TINT_DARK, - position: "absolute", - top: 20, - }, - settingsView: { backgroundColor: colors.TOOLBAR_COLOR_ALT }, - displayOptionsText: { padding: 7 }, - bottomSheetTitle: { - textAlign: "center", - fontSize: 20, - padding: 20, - borderTopLeftRadius: 20, - borderTopRightRadius: 20, - }, - titleInfoStyle: { - fontSize: 12, - }, - end: { padding: 40 }, - avatarStyle: { width: "100%", height: "100%", resizeMode: "contain" }, - viewWrapper: { - justifyContent: "center", - marginTop: "auto", - marginLeft: "auto", - marginRight: "auto", - bottom: 0, - borderTopLeftRadius: 20, - borderTopRightRadius: 20, - overflow: "hidden", - }, - width_100: { - width: "98%", - }, - width_90: { - width: "70%", - }, - blurViewStyle: { position: "absolute", top: 0, bottom: 0, left: 0, right: 0 }, - androidViewWrapper: { - flex: 1, - justifyContent: "flex-end", - backgroundColor: "transparent", - width: "100%", - }, - databaseUpdateBannerWrapper: { - fles: 1, - flexDirection: "row", - backgroundColor: colors.HEADER_COLOR_1_LIGHT, - justifyContent: "center", - alignItems: "center", - padding: 5, - }, - baniDbImage: { width: 20, height: 20, marginRight: 10 }, - updateText: { color: colors.WHITE_TEXT, fontSize: 14 }, -}); -export default styles; diff --git a/src/common/TrackPlayerUtils.js b/src/common/TrackPlayerUtils.js new file mode 100644 index 00000000..ec68fde4 --- /dev/null +++ b/src/common/TrackPlayerUtils.js @@ -0,0 +1,168 @@ +import TrackPlayer, { RepeatMode, AppKilledPlaybackBehavior } from "react-native-track-player"; +import { logError, logMessage } from "./index"; + +// Singleton service to manage TrackPlayer initialization +class TrackPlayerService { + constructor() { + this.isInitialized = false; + this.initPromise = null; + this.activeListeners = new Set(); + } + + async initialize() { + // Return existing promise if already initializing + if (this.initPromise) { + return this.initPromise; + } + + // Return immediately if already initialized + if (this.isInitialized) { + return Promise.resolve(); + } + + // Create initialization promise + this.initPromise = (async () => { + try { + logMessage("Initializing TrackPlayer service..."); + + // Setup the player with optimized configuration + // setupPlayer() will throw if already initialized, so we catch it + await TrackPlayer.setupPlayer({ + waitForBuffer: false, // Don't wait for buffer on startup for better performance + maxCacheSize: 512, // Reduced cache for faster startup + iosCategory: "playback", + alwaysPauseOnInterruption: true, + }); + + // Set repeat mode + await TrackPlayer.setRepeatMode(RepeatMode.Off); + + // Configure capabilities + await TrackPlayer.updateOptions({ + android: { + appKilledPlaybackBehavior: AppKilledPlaybackBehavior.StopPlaybackAndRemoveNotification, + }, + capabilities: [ + TrackPlayer.Capability.Play, + TrackPlayer.Capability.Pause, + TrackPlayer.Capability.SkipToNext, + TrackPlayer.Capability.SkipToPrevious, + TrackPlayer.Capability.Stop, + TrackPlayer.Capability.SeekTo, + ], + compactCapabilities: [ + TrackPlayer.Capability.Play, + TrackPlayer.Capability.Pause, + TrackPlayer.Capability.SkipToNext, + ], + }); + + this.isInitialized = true; + logMessage("TrackPlayer service initialized successfully"); + } catch (error) { + // If setupPlayer throws because it's already initialized, that's okay + if ( + error?.message?.includes("already initialized") || + error?.code === "player_already_initialized" + ) { + this.isInitialized = true; + logMessage("TrackPlayer already initialized"); + } else { + logError(`TrackPlayer initialization failed: ${error?.message || "Unknown error"}`); + this.isInitialized = false; + throw error; + } + } finally { + this.initPromise = null; + } + })(); + + return this.initPromise; + } + + async cleanup() { + try { + logMessage("Cleaning up TrackPlayer service..."); + + // Stop any active playback + await TrackPlayer.stop(); + await TrackPlayer.reset(); + + this.isInitialized = false; + logMessage("TrackPlayer service cleaned up successfully"); + } catch (error) { + logError(`TrackPlayer cleanup failed: ${error?.message || "Unknown error"}`); + } + } + + getState() { + return { + isInitialized: this.isInitialized, + }; + } +} + +// Export singleton instance +const trackPlayerService = new TrackPlayerService(); + +export const TrackPlayerSetup = async () => { + return trackPlayerService.initialize(); +}; + +export const TrackPlayerCleanup = async () => { + return trackPlayerService.cleanup(); +}; + +export const getTrackPlayerState = () => { + return trackPlayerService.getState(); +}; + +export const addTrack = async (track) => { + try { + // Validate track object + if (!track.url) { + logError("Track URL is missing or empty"); + throw new Error("Track URL is missing or empty"); + } + if (!track.id) { + logError("Track ID is missing"); + } + + await TrackPlayer.add(track); + } catch (error) { + logError(`โŒ Error adding track to TrackPlayer: ${error}`); + throw error; // Re-throw to handle upstream + } +}; + +export const playTrack = async () => { + try { + await TrackPlayer.play(); + } catch (error) { + logError(error); + } +}; + +export const pauseTrack = async () => { + try { + await TrackPlayer.pause(); + } catch (error) { + logError(`Error pausing track: ${error}`); + } +}; + +export const stopTrack = async () => { + try { + await TrackPlayer.stop(); + } catch (error) { + logError(`Error stopping track: ${error}`); + } +}; + +export const resetPlayer = async () => { + try { + await TrackPlayer.reset(); + } catch (error) { + logError(`Error resetting player: ${error}`); + } +}; diff --git a/src/common/actions/actionTypes.js b/src/common/actions/actionTypes.js index 92c5fb32..0f671b33 100644 --- a/src/common/actions/actionTypes.js +++ b/src/common/actions/actionTypes.js @@ -21,6 +21,7 @@ export const TOGGLE_ENGLISH_TRANSLATION = "TOGGLE_ENGLISH_TRANSLATION"; export const TOGGLE_PUNJABI_TRANSLATION = "TOGGLE_PUNJABI_TRANSLATION"; export const TOGGLE_SPANISH_TRANSLATION = "TOGGLE_SPANISH_TRANSLATION"; export const SET_BOOKMARK_POSITION = "SET_BOOKMARK_POSITION"; +export const SET_BOOKMARK_SEQUENCE_STRING = "SET_BOOKMARK_SEQUENCE_STRING"; export const TOGGLE_REMINDERS = "TOGGLE_REMINDERS"; export const SET_REMINDER_BANIS = "SET_REMINDER_BANIS"; export const SET_REMINDER_SOUND = "SET_REMINDER_SOUND"; @@ -32,3 +33,16 @@ export const SET_BANI_ORDER = "SET_BANI_ORDER"; export const SET_SCROLL_POSITION = "SET_SCROLL_POSITION"; export const TOGGLE_HEADER_FOOTER = "TOGGLE_HEADER_FOOTER"; export const TOGGLE_DATABASE_UPDATE_AVAILABLE = "TOGGLE_DATABASE_UPDATE_AVAILABLE"; +export const TOGGLE_AUDIO = "TOGGLE_AUDIO"; +export const TOGGLE_AUDIO_AUTO_PLAY = "TOGGLE_AUDIO_AUTO_PLAY"; +export const TOGGLE_AUDIO_SYNC_SCROLL = "TOGGLE_AUDIO_SYNC_SCROLL"; +export const SET_DEFAULT_AUDIO = "SET_DEFAULT_AUDIO"; +export const SET_AUDIO_PLAYBACK_SPEED = "SET_AUDIO_PLAYBACK_SPEED"; +export const SET_CURRENT_BANI = "SET_CURRENT_BANI"; + +// Manifest actions +export const SET_AUDIO_MANIFEST = "SET_AUDIO_MANIFEST"; + +// Audio progress actions +export const SET_AUDIO_PROGRESS = "SET_AUDIO_PROGRESS"; +export const CLEAR_AUDIO_PROGRESS = "CLEAR_AUDIO_PROGRESS"; diff --git a/src/common/actions/index.js b/src/common/actions/index.js index d7314eef..ea7f3303 100644 --- a/src/common/actions/index.js +++ b/src/common/actions/index.js @@ -1,7 +1,7 @@ -import * as actionTypes from "./actionTypes"; -import STRINGS from "../localization"; -import { trackSettingEvent } from "../firebase/analytics"; import constant from "../constant"; +import { trackSettingEvent, trackAudioEvent, trackArtist } from "../firebase/analytics"; +import STRINGS from "../localization"; +import * as actionTypes from "./actionTypes"; export const toggleNightMode = (value) => { trackSettingEvent(constant.NIGHT_MODE, value); @@ -40,6 +40,36 @@ export const toggleAutoScroll = (value) => { return { type: actionTypes.TOGGLE_AUTO_SCROLL, value }; }; +export const toggleAudio = (value) => { + trackSettingEvent(constant.AUDIO, value); + return { type: actionTypes.TOGGLE_AUDIO, value }; +}; + +export const toggleAudioAutoPlay = (value) => { + trackAudioEvent(constant.AUDIO_AUTO_PLAY, value); + return { type: actionTypes.TOGGLE_AUDIO_AUTO_PLAY, value }; +}; + +export const toggleAudioSyncScroll = (value) => { + trackAudioEvent(constant.AUDIO_SYNC_SCROLL, value); + return { type: actionTypes.TOGGLE_AUDIO_SYNC_SCROLL, value }; +}; + +export const setDefaultAudio = (audio, shabadId) => { + trackArtist(shabadId, audio.displayName); + const value = { [shabadId]: audio }; + return { type: actionTypes.SET_DEFAULT_AUDIO, value }; +}; + +export const setAudioPlaybackSpeed = (value) => { + trackAudioEvent("audioPlaybackSpeed", value); + return { type: actionTypes.SET_AUDIO_PLAYBACK_SPEED, value }; +}; + +export const setCurrentBani = (bani) => { + return { type: actionTypes.SET_CURRENT_BANI, value: bani }; +}; + export const toggleStatusBar = (value) => { trackSettingEvent(constant.STATUS_BAR, value); return { type: actionTypes.TOGGLE_STATUS_BAR, value }; @@ -110,6 +140,10 @@ export const setBookmarkPosition = (value) => { trackSettingEvent(constant.BOOKMARKS, value); return { type: actionTypes.SET_BOOKMARK_POSITION, value }; }; + +export const setBookmarkSequenceString = (value) => { + return { type: actionTypes.SET_BOOKMARK_SEQUENCE_STRING, value }; +}; export const toggleReminders = (value) => { trackSettingEvent(constant.REMINDERS, value); return { type: actionTypes.TOGGLE_REMINDERS, value }; @@ -124,6 +158,7 @@ export const setReminderSound = (value) => { }; export const setAutoScrollSpeed = (speed, shabad) => { + trackSettingEvent(constant.AUTO_SCROLL_SPEED, speed); const value = { [shabad]: speed }; return { type: actionTypes.SET_AUTO_SCROLL_SPEED, value }; }; @@ -150,3 +185,26 @@ export const toggleHeaderFooter = (value) => { export const toggleDatabaseUpdateAvailable = (value) => { return { type: actionTypes.TOGGLE_DATABASE_UPDATE_AVAILABLE, value }; }; + +// Manifest actions +export const setAudioManifest = (baniId, tracks) => { + return { + type: actionTypes.SET_AUDIO_MANIFEST, + payload: { baniId, tracks }, + }; +}; + +// Audio progress actions +export const setAudioProgress = (baniId, trackId, position, sequence) => { + return { + type: actionTypes.SET_AUDIO_PROGRESS, + payload: { baniId, trackId, position, sequence }, + }; +}; + +export const clearAudioProgress = (baniId) => { + return { + type: actionTypes.CLEAR_AUDIO_PROGRESS, + payload: { baniId }, + }; +}; diff --git a/src/common/colors.js b/src/common/colors.js index 696de164..3782471f 100644 --- a/src/common/colors.js +++ b/src/common/colors.js @@ -1,59 +1,9 @@ export default { - TOOLBAR_COLOR: "#2a3381", - READER_HEADER_COLOR: "#171d47dd", - READER_FOOTER_COLOR: "#171d47dd", - READER_STATUS_BAR_COLOR: "#363C5D", - READER_STATUS_BAR_COLOR_NIGHT_MODE: "#141a3c", - TOOLBAR_COLOR_ALT: "#DEBB0A", - TOOLBAR_COLOR_ALT_NIGHT_MODE: "#99852c", - TOOLBAR_COLOR_ALT2: "#003436", - TOOLBAR_TINT: "#faf9f6", - TOOLBAR_TINT_DARK: "#121212", - SETTING_SWITCH_COLOR: "#5195ea", - SETTING_SWITCH_THUMB: "#A7CAF8", - SETTING_BACKGROUND_COLOR: "#efeff4", - ACTIVE_VIEW_COLOR_NIGHT_MODE: "#2d2d2d", - INACTIVE_VIEW_COLOR_NIGHT_MODE: "#232323", - ACTIVE_VIEW_COLOR: "#C7C7D7", - INACTIVE_VIEW_COLOR: "#e9e9ee", - MODAL_BACKGROUND_NIGHT_MODE: "#202124", - MODAL_BACKGROUND: "#e9e9ee", - MODAL_ACCENT_NIGHT_MODE: "#2581df", - MODAL_ACCENT_NIGHT_MODE_ALT: "#5195ea", - MODAL_TEXT_NIGHT_MODE: "#faf9f6", - MODAL_TEXT: "#121212", - ENABELED_TEXT_COLOR_NIGHT_MODE: "#2581df", - DISABLED_TEXT_COLOR_NIGHT_MODE: "#a3a3a3", - ENABELED_TEXT_COLOR: "#2581df", - DISABLED_TEXT_COLOR: "#a3a3a3", - COMPONENT_COLOR_NIGHT_MODE: "#fefefe", - COMPONENT_COLOR: "#232323", + // These can be gradually migrated to the new semantic names above VISHRAM_SHORT: "#16a085", VISHRAM_LONG: "#d35400", VISHRAM_SHORT_GRADIENT: "rgba(22, 160, 133,1.0)", VISHRAM_LONG_GRADIENT: "rgba(211, 84, 0,1.0)", - WHITE_COLOR: "#faf9f6", - NIGHT_BLACK: "#121212", - UNDERLAY_COLOR: "#009bff", - BANI_ORDER_BACK_COLOR: "eee", - IOS_SHADOW_COLOR: "rgba(0,0,0,0.2)", - LIGHT_MODE_COLOR: "#222222", - HOME_BACK_COLOR: "#faf9f6", SLIDER_TRACK_MAX_TINT: "#464646", SLIDER_TRACK_MIN_TINT: "#BFBFBF", - VIEW_BACK_COLOR: "#464646", - VISHRAM_BASIC: "#c0392b", - MODAL_BACKGROUND_COLOR: "rgba(0,0,0,0.5)", - LABEL_COLORS: "#faf9f6", - NIGHT_GREY_COLOR: "#464646", - HEADER_COLOR_1_DARK: "#77baff", - HEADER_COLOR_1_LIGHT: "#0066ff", - HEADER_COLOR_2_LIGHT: "#727272", - BANIDB_LIGHT: "#eaa040", - SHADOW_COLOR: "#000", - LIGHT_GRAY: "#aaa", - WHITE_TEXT: "#fff", - ANIMATION_STROKE_LIGHT: "#e6e6e6", - ANIMATION_STROKE_ACTIVE: "#007AFF", - NIGHT_OPACITY_BLACK: "rgba(0, 0, 0, 0.5)", }; diff --git a/src/common/components/BackIconComponent/index.jsx b/src/common/components/BackIconComponent/index.jsx new file mode 100644 index 00000000..5c41db47 --- /dev/null +++ b/src/common/components/BackIconComponent/index.jsx @@ -0,0 +1,29 @@ +import React, { useCallback } from "react"; +import { Pressable } from "react-native"; +import { useNavigation } from "@react-navigation/native"; +import PropTypes from "prop-types"; +import { BackArrowIcon } from "@common/icons"; + +const BackIconComponent = ({ size, color }) => { + const navigation = useNavigation(); + + const handleBackPress = useCallback(() => { + navigation.goBack(); + }, []); + + return ( + + + + ); +}; + +BackIconComponent.defaultProps = { + size: 25, +}; +BackIconComponent.propTypes = { + size: PropTypes.number, + color: PropTypes.string.isRequired, +}; + +export default BackIconComponent; diff --git a/src/common/components/BaniLengthSelector/BaniLengthSelector.jsx b/src/common/components/BaniLengthSelector/BaniLengthSelector.jsx index 9f925d2b..38fbd3bd 100644 --- a/src/common/components/BaniLengthSelector/BaniLengthSelector.jsx +++ b/src/common/components/BaniLengthSelector/BaniLengthSelector.jsx @@ -1,14 +1,15 @@ import React from "react"; -import { View, Text, Pressable, Alert } from "react-native"; +import { View, Pressable, Alert } from "react-native"; import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context"; -import { Icon } from "@rneui/themed"; import { useDispatch } from "react-redux"; -import STRINGS from "../../localization"; -import styles from "./style"; -import colors from "../../colors"; +import { Icon } from "@rneui/themed"; +import { CustomText, STRINGS, useThemedStyles, useTheme } from "@common"; import { setBaniLength } from "../../actions"; +import createStyles from "./style"; const BaniLengthSelector = () => { + const styles = useThemedStyles(createStyles); + const { theme } = useTheme(); const baniLengths = [STRINGS.short, STRINGS.medium, STRINGS.long, STRINGS.extra_long]; const dispatch = useDispatch(); @@ -25,19 +26,19 @@ const BaniLengthSelector = () => { - {STRINGS.khalsa_sundar_gutka} - {STRINGS.bani_length_message_1} - {STRINGS.bani_length_message_2} - {STRINGS.choose_your_preference} + {STRINGS.khalsa_sundar_gutka} + {STRINGS.bani_length_message_1} + {STRINGS.bani_length_message_2} + {STRINGS.choose_your_preference} {baniLengths.map((buttonText) => ( handleOnpress(buttonText)}> - {buttonText} + {buttonText} ))} - - {STRINGS.need_help_deciding} - {STRINGS.click_more_info} + + {STRINGS.need_help_deciding} + {STRINGS.click_more_info} diff --git a/src/common/components/BaniLengthSelector/style.jsx b/src/common/components/BaniLengthSelector/style.jsx index e202aadb..d19a034d 100644 --- a/src/common/components/BaniLengthSelector/style.jsx +++ b/src/common/components/BaniLengthSelector/style.jsx @@ -1,57 +1,55 @@ -import { StyleSheet } from "react-native"; -import colors from "../../colors"; -import constant from "../../constant"; - -const styles = StyleSheet.create({ +const createStyles = (theme) => ({ heading: { - color: colors.TOOLBAR_TINT, - fontFamily: constant.GURBANI_AKHAR_THICK_TRUE, + color: theme.staticColors.WHITE_COLOR, + fontFamily: theme.typography.fonts.gurbaniThick, textAlign: "center", - fontSize: 52, + fontSize: theme.typography.sizes.massive + theme.spacing.xl, }, viewWrapper: { - margin: 10, + margin: theme.spacing.md, }, wrapper: { flex: 1, - backgroundColor: colors.TOOLBAR_COLOR, + backgroundColor: theme.colors.primary, }, baniLengthMessage: { - marginTop: 15, - color: colors.TOOLBAR_TINT, - fontSize: 14, + marginTop: theme.spacing.lg, + color: theme.staticColors.WHITE_COLOR, + fontSize: theme.typography.sizes.md, }, textPreferrence: { - marginTop: 15, - color: colors.TOOLBAR_COLOR_ALT, - fontWeight: "bold", - fontSize: 18, + marginTop: theme.spacing.lg, + color: theme.staticColors.WHITE_COLOR, + fontWeight: theme.typography.weights.bold, + fontSize: theme.typography.sizes.xl, }, button: { - backgroundColor: colors.WHITE_COLOR, - color: colors.TOOLBAR_COLOR, - padding: 15, - marginTop: 15, - fontSize: 24, - fontWeight: "bold", + backgroundColor: theme.colors.surface, + color: theme.colors.primaryText, + padding: theme.spacing.lg, + marginTop: theme.spacing.lg, + fontSize: theme.typography.sizes.xxxl, + fontWeight: theme.typography.weights.bold, textAlign: "center", textTransform: "uppercase", + borderRadius: theme.components.button.borderRadius, + minHeight: theme.components.button.minHeight, }, helpText: { - color: colors.TOOLBAR_COLOR_ALT, - fontWeight: "bold", + color: theme.colors.primaryVariant, + fontWeight: theme.typography.weights.bold, fontStyle: "italic", - fontSize: 12, + fontSize: theme.typography.sizes.sm, }, moreInfo: { - color: colors.TOOLBAR_TINT, - fontWeight: "normal", - fontSize: 12, + color: theme.colors.primaryText, + fontWeight: theme.typography.weights.normal, + fontSize: theme.typography.sizes.sm, }, helpWrapper: { flexDirection: "row", alignItems: "center", - marginTop: 15, + marginTop: theme.spacing.lg, }, }); -export default styles; +export default createStyles; diff --git a/src/common/components/BaniList/BaniList.jsx b/src/common/components/BaniList/BaniList.jsx index 63a555be..85bbb12e 100644 --- a/src/common/components/BaniList/BaniList.jsx +++ b/src/common/components/BaniList/BaniList.jsx @@ -1,17 +1,20 @@ import React, { useCallback, useEffect, useState } from "react"; import { FlatList, Dimensions, Platform } from "react-native"; +import { useSelector } from "react-redux"; import { ListItem, Avatar } from "@rneui/themed"; +import createStyles from "@settings/styles"; import PropTypes from "prop-types"; -import { useSelector } from "react-redux"; -import baseFontSize from "../../helpers"; -import colors from "../../colors"; -import { styles } from "../../../Settings/styles"; +import constant from "@common/constant"; +import useTheme from "@common/context"; +import useThemedStyles from "@common/hooks/useThemedStyles"; +import { convertToUnicode, baseFontSize, ListItemTitle } from "@common"; const BaniList = React.memo(({ data, onPress }) => { + const { theme } = useTheme(); + const styles = useThemedStyles(createStyles); const fontSize = useSelector((state) => state.fontSize); const fontFace = useSelector((state) => state.fontFace); const isTransliteration = useSelector((state) => state.isTransliteration); - const isNightMode = useSelector((state) => state.isNightMode); const [isPotrait, toggleIsPotrait] = useState(true); const checkPotrait = () => { @@ -24,13 +27,31 @@ const BaniList = React.memo(({ data, onPress }) => { }); return () => subscription.remove(); }, []); + const isUnicode = fontFace === constant.BALOO_PAAJI; + + const getBaniTuk = (row) => { + if (!row || !row.item) { + return ""; + } + if (isTransliteration) { + return row.item.translit; + } + if (isUnicode) { + if (row?.item?.gurmukhiUni) { + return row.item.gurmukhiUni; + } + return convertToUnicode(row.item.gurmukhi); + } + return row.item.gurmukhi; + }; + const renderBanis = useCallback( (row) => { return ( onPress(row)} > @@ -41,33 +62,31 @@ const BaniList = React.memo(({ data, onPress }) => { /> )} - - {isTransliteration ? row.item.translit : row.item.gurmukhi} - + /> {row.item.tukGurmukhi && ( - - {isTransliteration ? row.item.tukTranslit : row.item.tukGurmukhi} - + /> )} ); }, - [isNightMode, fontSize, fontFace, isTransliteration] + [theme, fontSize, fontFace, isTransliteration] ); return ( diff --git a/src/common/components/BaniList/baniOrderHelper.js b/src/common/components/BaniList/baniOrderHelper.js index 877b5420..0f83701c 100644 --- a/src/common/components/BaniList/baniOrderHelper.js +++ b/src/common/components/BaniList/baniOrderHelper.js @@ -6,6 +6,7 @@ const extractBaniDetails = (baniItem) => { id: baniItem.id, gurmukhi: baniItem.gurmukhi, translit: baniItem.translit, + gurmukhiUni: baniItem.gurmukhiUni, }; }; const orderedBani = (baniList, baniOrder) => { @@ -34,7 +35,12 @@ const orderedBani = (baniList, baniOrder) => { }, []); return folder.length - ? { gurmukhi: element.gurmukhi, translit: element.translit, folder } + ? { + gurmukhiUni: element.gurmukhiUni, + gurmukhi: element.gurmukhi, + translit: element.translit, + folder, + } : null; }) // Filter out any nulls in case an ID did not match diff --git a/src/common/components/BottomNavigation/index.jsx b/src/common/components/BottomNavigation/index.jsx new file mode 100644 index 00000000..dc293b2b --- /dev/null +++ b/src/common/components/BottomNavigation/index.jsx @@ -0,0 +1,175 @@ +import React, { useState, useEffect, useCallback } from "react"; +import { View, Pressable } from "react-native"; +import { useDispatch, useSelector } from "react-redux"; +import { useNavigation } from "@react-navigation/native"; +import PropTypes from "prop-types"; +import useTheme from "@common/context"; +import useThemedStyles from "@common/hooks/useThemedStyles"; +import { HomeIcon, SettingsIcon, MusicIcon, ReadIcon } from "@common/icons"; +import { CustomText, actions, constant, STRINGS, SafeArea } from "@common"; +import createStyles from "./style"; + +const BottomNavigation = ({ activeKey }) => { + const navigation = useNavigation(); + const dispatch = useDispatch(); + const { theme } = useTheme(); + const styles = useThemedStyles(createStyles); + const isAudio = useSelector((state) => state.isAudio); + const [isSettings, setIsSettings] = useState(false); + const [previousRouteName, setPreviousRouteName] = useState(null); + + // Helper function to get current route name + const getCurrentRouteName = useCallback(() => { + const navState = navigation.getState(); + return navState?.routes[navState?.index]?.name; + }, [navigation]); + + useEffect(() => { + const updateIsSettings = () => { + const state = navigation.getState?.(); + if (!state) return; + + const topRoute = state.routes[state.index]; + let currentRouteName = topRoute?.name; + + // Handle nested navigators just in case + if (topRoute?.state && typeof topRoute.state.index === "number") { + const nestedRoute = topRoute.state.routes[topRoute.state.index]; + currentRouteName = nestedRoute?.name ?? currentRouteName; + } + + // When entering Settings, check the previous route in navigation stack + if (currentRouteName === constant.SETTINGS) { + // Get the previous route from navigation state + if (state.index > 0) { + const prevRoute = state.routes[state.index - 1]; + let prevRouteName = prevRoute?.name; + if (prevRoute?.state && typeof prevRoute.state.index === "number") { + const nestedRoute = prevRoute.state.routes[prevRoute.state.index]; + prevRouteName = nestedRoute?.name ?? prevRouteName; + } + setPreviousRouteName(prevRouteName); + } + } else { + // Update previous route when not on Settings + setPreviousRouteName(currentRouteName); + } + + setIsSettings(currentRouteName === constant.SETTINGS); + }; + + // Run once on mount + updateIsSettings(); + + // Subscribe to navigation state changes + const unsubscribe = + navigation.addListener?.("state", () => { + updateIsSettings(); + }) || undefined; + + return () => { + if (unsubscribe) { + unsubscribe(); + } + }; + }, [navigation]); + + const navigationItems = [ + { + key: "Home", + icon: HomeIcon, + handlePress: () => { + navigation.popToTop(); + }, + text: STRINGS.HOME, + }, + { + key: "Read", + icon: ReadIcon, + handlePress: () => { + const currentNavRoute = getCurrentRouteName(); + + if (currentNavRoute === constant.SETTINGS) { + navigation.goBack(); + } + if (isAudio) { + dispatch(actions.toggleAudio(false)); + } + }, + text: STRINGS.READ, + }, + { + key: "Music", + icon: MusicIcon, + handlePress: () => { + const currentNavRoute = getCurrentRouteName(); + + if (currentNavRoute === constant.SETTINGS) { + navigation.goBack(); + } + + dispatch(actions.toggleAutoScroll(false)); + + // If coming from Settings and previous route was Reader, keep audio ON + if (currentNavRoute === constant.SETTINGS && isAudio) { + dispatch(actions.toggleAudio(true)); + } else { + dispatch(actions.toggleAudio(!isAudio)); + } + }, + text: STRINGS.MUSIC, + }, + { + key: "Settings", + icon: SettingsIcon, + handlePress: () => { + navigation.navigate(constant.SETTINGS); + }, + text: STRINGS.SETTINGS, + }, + ]; + + // Filter out Read and Music when on Settings page, but keep them if previous route was Read + const shouldHideReadAndMusic = isSettings && previousRouteName !== constant.READER; + const filteredNavigationItems = shouldHideReadAndMusic + ? navigationItems.filter((item) => item.key !== "Read" && item.key !== "Music") + : navigationItems; + + return ( + + + + {filteredNavigationItems.map((item) => { + const IconComponent = item.icon; + + return ( + + + {activeKey !== item.key && ( + {item.text} + )} + + ); + })} + + + + ); +}; + +BottomNavigation.propTypes = { + activeKey: PropTypes.string.isRequired, +}; + +export default BottomNavigation; diff --git a/src/common/components/BottomNavigation/index.test.jsx b/src/common/components/BottomNavigation/index.test.jsx new file mode 100644 index 00000000..25ea86f7 --- /dev/null +++ b/src/common/components/BottomNavigation/index.test.jsx @@ -0,0 +1,236 @@ +// BottomNavigation.test.jsx +import React from "react"; + +import { render, fireEvent } from "@testing-library/react-native"; + +import { getMockDispatch, setMockState } from "@common/test-utils/mocks/react-redux"; + +import BottomNavigation from "./index"; + +// Mock styles module used by useThemedStyles (not strictly necessary because we mock the hook) +jest.mock("./style", () => jest.fn()); + +// Mock useNavigation hook +let mockNavigation; +const mockUseNavigation = jest.fn(() => mockNavigation); + +jest.mock("@react-navigation/native", () => ({ + useNavigation: () => mockUseNavigation(), +})); + +// --- Helpers --- + +const createNavigation = ({ currentRoute = "Home" } = {}) => { + const navigate = jest.fn(); + const popToTop = jest.fn(); + const goBack = jest.fn(); + const addListener = jest.fn(() => jest.fn()); // Returns unsubscribe function + const routes = [{ name: "Home" }, { name: "Reader" }, { name: "Settings" }]; + let index = 0; + if (currentRoute === "Reader") { + index = 1; + } else if (currentRoute === "Settings") { + index = 2; + } + const getState = jest.fn(() => ({ + routes, + index, + })); + return { navigate, getState, popToTop, goBack, addListener }; +}; + +describe("BottomNavigation", () => { + const mockDispatch = getMockDispatch(); + + beforeEach(() => { + jest.clearAllMocks(); + setMockState({ isAudio: false }); + mockNavigation = createNavigation(); + mockUseNavigation.mockReturnValue(mockNavigation); + }); + + test("renders four buttons with correct accessibility labels", () => { + const { getByLabelText } = render(); + + expect(getByLabelText("bottomnav-Home")).toBeTruthy(); + expect(getByLabelText("bottomnav-Read")).toBeTruthy(); + expect(getByLabelText("bottomnav-Music")).toBeTruthy(); + expect(getByLabelText("bottomnav-Settings")).toBeTruthy(); + }); + + test("shows labels for non-active items and hides label for the active item", () => { + const { queryByText } = render(); + + // Active "Music" label should be hidden (component shows label only when NOT active) + expect(queryByText("Music")).toBeNull(); + + // Others should be visible + expect(queryByText("Home")).not.toBeNull(); + expect(queryByText("Read")).not.toBeNull(); + expect(queryByText("Settings")).not.toBeNull(); + }); + + test("pressing Home navigates to Home", () => { + const { getByLabelText } = render(); + + fireEvent.press(getByLabelText("bottomnav-Home")); + + expect(mockNavigation.popToTop).toHaveBeenCalled(); + }); + + test("pressing Read when audio is on toggles audio to false", () => { + setMockState({ isAudio: true }); + mockNavigation = createNavigation({ currentRoute: "Home" }); + mockUseNavigation.mockReturnValue(mockNavigation); + + const { getByLabelText } = render(); + + fireEvent.press(getByLabelText("bottomnav-Read")); + + expect(mockDispatch).toHaveBeenCalledWith({ type: "TOGGLE_AUDIO", payload: false }); + }); + + test("pressing Read when audio is off does not toggle audio", () => { + setMockState({ isAudio: false }); + mockNavigation = createNavigation({ currentRoute: "Home" }); + mockUseNavigation.mockReturnValue(mockNavigation); + + const { getByLabelText } = render(); + + fireEvent.press(getByLabelText("bottomnav-Read")); + + expect(mockDispatch).not.toHaveBeenCalledWith({ type: "TOGGLE_AUDIO", payload: false }); + }); + + test("pressing Read from Settings calls goBack", () => { + setMockState({ isAudio: false }); + mockNavigation = createNavigation({ currentRoute: "Settings" }); + mockUseNavigation.mockReturnValue(mockNavigation); + + const { getByLabelText } = render(); + + fireEvent.press(getByLabelText("bottomnav-Read")); + + expect(mockNavigation.goBack).toHaveBeenCalled(); + }); + + test("pressing Music when NOT on Reader or Settings dispatches actions", () => { + setMockState({ isAudio: false }); + mockNavigation = createNavigation({ currentRoute: "Home" }); + mockUseNavigation.mockReturnValue(mockNavigation); + + const { getByLabelText } = render(); + + fireEvent.press(getByLabelText("bottomnav-Music")); + + // Dispatches: autoScroll=false, audio toggled from false -> true + expect(mockDispatch).toHaveBeenCalledWith({ type: "TOGGLE_AUTO_SCROLL", payload: false }); + expect(mockDispatch).toHaveBeenCalledWith({ type: "TOGGLE_AUDIO", payload: true }); + }); + + test("pressing Music when ALREADY on Reader dispatches actions", () => { + setMockState({ isAudio: false }); + mockNavigation = createNavigation({ currentRoute: "Reader" }); + mockUseNavigation.mockReturnValue(mockNavigation); + + const { getByLabelText } = render(); + + fireEvent.press(getByLabelText("bottomnav-Music")); + + // Dispatches: autoScroll=false, audio toggled from false -> true + expect(mockDispatch).toHaveBeenCalledWith({ type: "TOGGLE_AUTO_SCROLL", payload: false }); + expect(mockDispatch).toHaveBeenCalledWith({ type: "TOGGLE_AUDIO", payload: true }); + }); + + test("pressing Music from Settings calls goBack and keeps audio ON if audio was already on", () => { + setMockState({ isAudio: true }); + mockNavigation = createNavigation({ currentRoute: "Settings" }); + mockUseNavigation.mockReturnValue(mockNavigation); + + const { getByLabelText } = render(); + + fireEvent.press(getByLabelText("bottomnav-Music")); + + expect(mockNavigation.goBack).toHaveBeenCalled(); + expect(mockDispatch).toHaveBeenCalledWith({ type: "TOGGLE_AUTO_SCROLL", payload: false }); + expect(mockDispatch).toHaveBeenCalledWith({ type: "TOGGLE_AUDIO", payload: true }); + }); + + test("pressing Music from Settings calls goBack and toggles audio if audio was off", () => { + setMockState({ isAudio: false }); + mockNavigation = createNavigation({ currentRoute: "Settings" }); + mockUseNavigation.mockReturnValue(mockNavigation); + + const { getByLabelText } = render(); + + fireEvent.press(getByLabelText("bottomnav-Music")); + + expect(mockNavigation.goBack).toHaveBeenCalled(); + expect(mockDispatch).toHaveBeenCalledWith({ type: "TOGGLE_AUTO_SCROLL", payload: false }); + expect(mockDispatch).toHaveBeenCalledWith({ type: "TOGGLE_AUDIO", payload: true }); + }); + + test("pressing Settings navigates to Settings", () => { + const { getByLabelText } = render(); + + fireEvent.press(getByLabelText("bottomnav-Settings")); + + expect(mockNavigation.navigate).toHaveBeenCalledWith("Settings"); + }); + + test("pressing Music toggles audio based on current isAudio state", () => { + // Start with isAudio=true to verify toggle -> false + setMockState({ isAudio: true }); + mockNavigation = createNavigation({ currentRoute: "Reader" }); + mockUseNavigation.mockReturnValue(mockNavigation); + + const { getByLabelText } = render(); + + fireEvent.press(getByLabelText("bottomnav-Music")); + + // toggleAutoScroll(false) always + expect(mockDispatch).toHaveBeenCalledWith({ type: "TOGGLE_AUTO_SCROLL", payload: false }); + // toggled from true -> false + expect(mockDispatch).toHaveBeenCalledWith({ type: "TOGGLE_AUDIO", payload: false }); + }); + + test("As a user entering Settings from Home I want irrelevant tabs hidden So that navigation isn't confusing", () => { + // Simulate coming from Home (not Reader) + mockNavigation = createNavigation({ currentRoute: "Settings" }); + // Set up navigation state to have Home as previous route + mockNavigation.getState.mockReturnValue({ + routes: [{ name: "Home" }, { name: "Settings" }], + index: 1, + }); + mockUseNavigation.mockReturnValue(mockNavigation); + + const { getByLabelText, queryByLabelText } = render(); + + // Home and Settings should be visible + expect(getByLabelText("bottomnav-Home")).toBeTruthy(); + expect(getByLabelText("bottomnav-Settings")).toBeTruthy(); + + // Read and Music should be hidden on Settings page when coming from Home + expect(queryByLabelText("bottomnav-Read")).toBeNull(); + expect(queryByLabelText("bottomnav-Music")).toBeNull(); + }); + + test("As a user entering Settings from Reader I want Read and Music tabs to stay visible", () => { + // Simulate coming from Reader + mockNavigation = createNavigation({ currentRoute: "Settings" }); + // Set up navigation state to have Reader as previous route + mockNavigation.getState.mockReturnValue({ + routes: [{ name: "Home" }, { name: "Reader" }, { name: "Settings" }], + index: 2, + }); + mockUseNavigation.mockReturnValue(mockNavigation); + + const { getByLabelText } = render(); + + // All tabs should be visible when coming from Reader + expect(getByLabelText("bottomnav-Home")).toBeTruthy(); + expect(getByLabelText("bottomnav-Read")).toBeTruthy(); + expect(getByLabelText("bottomnav-Music")).toBeTruthy(); + expect(getByLabelText("bottomnav-Settings")).toBeTruthy(); + }); +}); diff --git a/src/common/components/BottomNavigation/style.js b/src/common/components/BottomNavigation/style.js new file mode 100644 index 00000000..8d1c91df --- /dev/null +++ b/src/common/components/BottomNavigation/style.js @@ -0,0 +1,34 @@ +const createStyles = (theme) => ({ + container: { + width: "100%", + backgroundColor: theme.colors.primary, + height: theme.components.bottomNavigation.height, + justifyContent: "center", + }, + navigationBar: { + flexDirection: "row", + justifyContent: "space-evenly", + width: "85%", + marginLeft: "auto", + marginRight: "auto", + gap: 25, + }, + iconContainer: { + flexBasis: 50, + height: 50, + alignItems: "center", + justifyContent: "center", + }, + activeIconContainer: { + backgroundColor: theme.staticColors.WHITE_COLOR, + borderRadius: 15, + padding: theme.spacing.lg, + }, + iconText: { + fontSize: theme.typography.sizes.sm, + color: theme.staticColors.WHITE_COLOR, + fontFamily: theme.typography.fonts.balooPaaji, + }, +}); + +export default createStyles; diff --git a/src/common/components/CustomText/index.jsx b/src/common/components/CustomText/index.jsx new file mode 100644 index 00000000..acd090eb --- /dev/null +++ b/src/common/components/CustomText/index.jsx @@ -0,0 +1,41 @@ +import React from "react"; +import { Text } from "react-native"; +import PropTypes from "prop-types"; +import useTheme from "@common/context"; + +const CustomText = ({ style, children, numberOfLines, onPress, onLongPress }) => { + const { theme } = useTheme(); + const textStyle = Array.isArray(style) + ? [{ fontFamily: theme.typography.fonts.balooPaaji }, ...style] + : [{ fontFamily: theme.typography.fonts.balooPaaji }, style]; + + return ( + + {children} + + ); +}; + +CustomText.propTypes = { + style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), + children: PropTypes.node, + numberOfLines: PropTypes.number, + onPress: PropTypes.func, + onLongPress: PropTypes.func, +}; + +CustomText.defaultProps = { + style: null, + children: null, + numberOfLines: null, + onPress: null, + onLongPress: null, +}; + +export default CustomText; diff --git a/src/common/components/FallbackComponent/index.jsx b/src/common/components/FallbackComponent/index.jsx index 4b9634b7..3a9c6229 100644 --- a/src/common/components/FallbackComponent/index.jsx +++ b/src/common/components/FallbackComponent/index.jsx @@ -1,22 +1,27 @@ -import React from "react"; -import { Button, Text, View, Linking } from "react-native"; -import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context"; +import React, { useEffect } from "react"; +import { Button, View, Linking } from "react-native"; import RNRestart from "react-native-restart"; -import STRINGS from "../../localization"; -import styles from "./styles"; -import useScreenAnalytics from "../../hooks/useScreenAnalytics"; +import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context"; +import useThemedStyles from "@common/hooks/useThemedStyles"; +import { CustomText } from "@common"; import constant from "../../constant"; -import { logMessage } from "../../firebase/crashlytics"; +import { trackScreenView } from "../../firebase/analytics"; +import STRINGS from "../../localization"; +import createStyles from "./styles"; const FallBack = () => { - logMessage(constant.FALLBACK); + const styles = useThemedStyles(createStyles); const { container, title, text, btnWrap } = styles; - useScreenAnalytics(constant.FALLBACK); + + useEffect(() => { + // Track screen view when error fallback is shown + trackScreenView(constant.FALLBACK_SCREEN, null, "Error Boundary Fallback Screen"); + }, []); return ( - {STRINGS.errorTitle} - {STRINGS.errorMessage} + {STRINGS.errorTitle} + {STRINGS.errorMessage}