diff --git a/README.md b/README.md
index a606760..6364607 100644
--- a/README.md
+++ b/README.md
@@ -15,20 +15,19 @@ apps
├─ Expo SDK 50
├─ React Native using React 18
├─ Navigation using Expo Router
- ├─ Tailwind using Nativewind
- └─ Typesafe API calls using tRPC
+ └─ Styling with Tamagui
packages
├─ tmdb
| └─ Typesafe API calls to The Movie Database
└─ provider-utils
└─ Typesafe API calls to the video providers
tooling
+ ├─ color
+ | └─ shared color palette
├─ eslint
| └─ shared, fine-grained, eslint presets
├─ prettier
| └─ shared prettier configuration
- ├─ tailwind
- | └─ shared tailwind configuration
└─ typescript
└─ shared tsconfig you can extend from
```
diff --git a/apps/expo/babel.config.js b/apps/expo/babel.config.js
index 42cdf24..ccd7e7f 100644
--- a/apps/expo/babel.config.js
+++ b/apps/expo/babel.config.js
@@ -2,10 +2,7 @@
module.exports = function (api) {
api.cache(true);
return {
- presets: [
- ["babel-preset-expo", { jsxImportSource: "nativewind" }],
- "nativewind/babel",
- ],
+ presets: ["babel-preset-expo"],
plugins: [
"react-native-reanimated/plugin",
[
diff --git a/apps/expo/metro.config.js b/apps/expo/metro.config.js
index 6a85d2a..259061d 100644
--- a/apps/expo/metro.config.js
+++ b/apps/expo/metro.config.js
@@ -1,19 +1,19 @@
// Learn more: https://docs.expo.dev/guides/monorepos/
const { getDefaultConfig } = require("expo/metro-config");
const { FileStore } = require("metro-cache");
-const { withNativeWind } = require("nativewind/metro");
+const { withTamagui } = require("@tamagui/metro-plugin");
const path = require("path");
module.exports = withTurborepoManagedCache(
withMonorepoPaths(
- withNativeWind(
+ withTamagui(
getDefaultConfig(__dirname, {
isCSSEnabled: true,
}),
{
- input: "./src/styles/global.css",
- configPath: "./tailwind.config.ts",
+ components: ["tamagui"],
+ config: "./tamagui.config.ts",
},
),
),
diff --git a/apps/expo/package.json b/apps/expo/package.json
index 1a16422..135b0a6 100644
--- a/apps/expo/package.json
+++ b/apps/expo/package.json
@@ -19,38 +19,42 @@
},
"dependencies": {
"@expo/metro-config": "^0.17.3",
+ "@movie-web/colors": "*",
"@movie-web/provider-utils": "*",
"@movie-web/tmdb": "*",
"@react-native-anywhere/polyfill-base64": "0.0.1-alpha.0",
"@react-navigation/native": "^6.1.9",
+ "@tamagui/animations-moti": "^1.91.4",
+ "@tamagui/babel-plugin": "^1.91.4",
+ "@tamagui/config": "^1.91.4",
+ "@tamagui/metro-plugin": "^1.91.4",
"@tanstack/react-query": "^5.22.2",
"class-variance-authority": "^0.7.0",
- "clsx": "^2.1.0",
- "expo": "~50.0.5",
+ "expo": "~50.0.13",
"expo-av": "~13.10.5",
"expo-brightness": "~11.8.0",
"expo-build-properties": "~0.11.1",
"expo-constants": "~15.4.5",
"expo-haptics": "~12.8.1",
+ "expo-linear-gradient": "^12.7.2",
"expo-linking": "~6.2.2",
"expo-navigation-bar": "^2.8.1",
- "expo-router": "~3.4.6",
+ "expo-router": "~3.4.8",
"expo-screen-orientation": "~6.4.1",
"expo-splash-screen": "~0.26.4",
"expo-status-bar": "~1.11.1",
+ "expo-system-ui": "^2.9.3",
"expo-web-browser": "^12.8.2",
"immer": "^10.0.3",
"iso-639-1": "^3.1.2",
- "nativewind": "^4.0.35",
"react": "18.2.0",
"react-dom": "18.2.0",
- "react-native": "0.73.2",
+ "react-native": "0.73.5",
"react-native-context-menu-view": "^1.14.1",
- "react-native-css-interop": "^0.0.35",
"react-native-gesture-handler": "~2.14.1",
+ "react-native-ios-modal": "^0.1.8",
"react-native-modal": "^13.0.1",
"react-native-paper": "^5.12.3",
- "react-native-progress": "^5.0.1",
"react-native-quick-base64": "^2.0.8",
"react-native-quick-crypto": "^0.6.1",
"react-native-reanimated": "~3.6.2",
@@ -60,7 +64,7 @@
"react-native-url-polyfill": "^2.0.0",
"react-native-web": "^0.19.10",
"subsrt-ts": "^2.1.2",
- "tailwind-merge": "^2.2.1",
+ "tamagui": "^1.91.4",
"zustand": "^4.4.7"
},
"devDependencies": {
@@ -69,7 +73,6 @@
"@babel/runtime": "^7.23.9",
"@movie-web/eslint-config": "workspace:^0.2.0",
"@movie-web/prettier-config": "workspace:^0.1.0",
- "@movie-web/tailwind-config": "workspace:^0.1.0",
"@movie-web/tsconfig": "workspace:^0.1.0",
"@tanstack/eslint-plugin-query": "^5.20.1",
"@types/babel__core": "^7.20.5",
@@ -77,7 +80,6 @@
"babel-plugin-module-resolver": "^5.0.0",
"eslint": "^8.56.0",
"prettier": "^3.1.1",
- "tailwindcss": "^3.4.0",
"typescript": "^5.3.3"
},
"eslintConfig": {
diff --git a/apps/expo/src/app/(tabs)/_layout.tsx b/apps/expo/src/app/(tabs)/_layout.tsx
index 5d0ca82..a3e1e7c 100644
--- a/apps/expo/src/app/(tabs)/_layout.tsx
+++ b/apps/expo/src/app/(tabs)/_layout.tsx
@@ -1,10 +1,9 @@
import { useRef } from "react";
-import { Platform, StyleSheet, View } from "react-native";
+import { Platform } from "react-native";
import * as Haptics from "expo-haptics";
import { Tabs } from "expo-router";
import * as ScreenOrientation from "expo-screen-orientation";
-
-import { defaultTheme } from "@movie-web/tailwind-config/themes";
+import { useTheme, View } from "tamagui";
import { MovieWebSvg } from "~/components/Icon";
import SvgTabBarIcon from "~/components/SvgTabBarIcon";
@@ -15,11 +14,13 @@ export default function TabLayout() {
// eslint-disable-next-line @typescript-eslint/no-empty-function
const focusSearchInputRef = useRef(() => {});
+ const theme = useTheme();
+
return (
({
tabPress: () => {
@@ -38,9 +39,9 @@ export default function TabLayout() {
})}
screenOptions={{
headerShown: false,
- tabBarActiveTintColor: defaultTheme.extend.colors.tabBar.active,
+ tabBarActiveTintColor: theme.tabBarIconFocused.val,
tabBarStyle: {
- backgroundColor: defaultTheme.extend.colors.tabBar.background,
+ backgroundColor: theme.tabBarBackground.val,
borderTopColor: "transparent",
borderTopRightRadius: 20,
borderTopLeftRadius: 20,
@@ -83,10 +84,16 @@ export default function TabLayout() {
tabBarLabel: "",
tabBarIcon: ({ focused }) => (
@@ -117,22 +124,3 @@ export default function TabLayout() {
);
}
-
-const styles = StyleSheet.create({
- searchTab: {
- top: 2,
- height: 56,
- width: 56,
- alignItems: "center",
- justifyContent: "center",
- overflow: "hidden",
- borderRadius: 100,
- textAlign: "center",
- },
- active: {
- backgroundColor: defaultTheme.extend.colors.tabBar.active,
- },
- inactive: {
- backgroundColor: defaultTheme.extend.colors.tabBar.inactive,
- },
-});
diff --git a/apps/expo/src/app/(tabs)/index.tsx b/apps/expo/src/app/(tabs)/index.tsx
index 7b55537..83877de 100644
--- a/apps/expo/src/app/(tabs)/index.tsx
+++ b/apps/expo/src/app/(tabs)/index.tsx
@@ -1,5 +1,5 @@
import React from "react";
-import { View } from "react-native";
+import { Text, View } from "tamagui";
import {
bookmarks,
@@ -7,15 +7,16 @@ import {
watching,
} from "~/components/item/ItemListSection";
import ScreenLayout from "~/components/layout/ScreenLayout";
-import { Text } from "~/components/ui/Text";
export default function HomeScreen() {
return (
-
+
- Home
+
+
+ Home
+
}
>
diff --git a/apps/expo/src/app/(tabs)/movie-web.tsx b/apps/expo/src/app/(tabs)/movie-web.tsx
index 8b7e445..b6aecdd 100644
--- a/apps/expo/src/app/(tabs)/movie-web.tsx
+++ b/apps/expo/src/app/(tabs)/movie-web.tsx
@@ -1,5 +1,6 @@
+import { Text } from "tamagui";
+
import ScreenLayout from "~/components/layout/ScreenLayout";
-import { Text } from "~/components/ui/Text";
export default function MovieWebScreen() {
return (
diff --git a/apps/expo/src/app/(tabs)/search.tsx b/apps/expo/src/app/(tabs)/search.tsx
index 9257e1a..cd44a33 100644
--- a/apps/expo/src/app/(tabs)/search.tsx
+++ b/apps/expo/src/app/(tabs)/search.tsx
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react";
-import { Keyboard, ScrollView, View } from "react-native";
+import { Keyboard, ScrollView } from "react-native";
import Animated, {
Easing,
useAnimatedStyle,
@@ -7,19 +7,14 @@ import Animated, {
withTiming,
} from "react-native-reanimated";
import { useQuery } from "@tanstack/react-query";
+import { Text, View } from "tamagui";
import { getMediaPoster, searchTitle } from "@movie-web/tmdb";
import type { ItemData } from "~/components/item/item";
import Item from "~/components/item/item";
-import {
- bookmarks,
- ItemListSection,
- watching,
-} from "~/components/item/ItemListSection";
import ScreenLayout from "~/components/layout/ScreenLayout";
import { SearchBar } from "~/components/ui/Searchbar";
-import { Text } from "~/components/ui/Text";
export default function HomeScreen() {
const [query, setQuery] = useState("");
@@ -113,36 +108,28 @@ export default function HomeScreen() {
>
- Search
+
+
+ Search
+
}
>
- {searchResultsLoaded ? (
+ {searchResultsLoaded && (
-
+
{data?.map((item, index) => (
-
+
))}
- ) : (
- 0 || watching.length > 0 ? true : false
- }
- >
-
-
-
)}
diff --git a/apps/expo/src/app/(tabs)/settings.tsx b/apps/expo/src/app/(tabs)/settings.tsx
index b94648f..16f2ea8 100644
--- a/apps/expo/src/app/(tabs)/settings.tsx
+++ b/apps/expo/src/app/(tabs)/settings.tsx
@@ -1,22 +1,142 @@
-import React, { useState } from "react";
-import { Text, View } from "react-native";
-import { Switch } from "react-native-paper";
+import type { SelectProps } from "tamagui";
+import React from "react";
+import { FontAwesome, MaterialIcons } from "@expo/vector-icons";
+import {
+ Adapt,
+ Label,
+ Select,
+ Separator,
+ Sheet,
+ Switch,
+ Text,
+ useTheme,
+ View,
+ XStack,
+ YStack,
+} from "tamagui";
+import type { ThemeStoreOption } from "~/stores/theme";
import ScreenLayout from "~/components/layout/ScreenLayout";
+import { useThemeStore } from "~/stores/theme";
+
+const themeOptions: ThemeStoreOption[] = [
+ "main",
+ "blue",
+ "gray",
+ "red",
+ "teal",
+];
export default function SettingsScreen() {
- const [isSwitchOn, setIsSwitchOn] = useState(true);
- const onToggleSwitch = () => setIsSwitchOn(!isSwitchOn);
-
return (
-
- Player
-
- Gesture Controls
-
-
+
+
+ Player
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
}
+
+export function ThemeSelector(props: SelectProps) {
+ const theme = useTheme();
+ const themeStore = useThemeStore((s) => s.theme);
+ const setTheme = useThemeStore((s) => s.setTheme);
+
+ return (
+
+ );
+}
diff --git a/apps/expo/src/app/[...missing].tsx b/apps/expo/src/app/[...missing].tsx
index 1844311..ec64eb6 100644
--- a/apps/expo/src/app/[...missing].tsx
+++ b/apps/expo/src/app/[...missing].tsx
@@ -1,19 +1,21 @@
-import { View } from "react-native";
import { Link, Stack } from "expo-router";
-
-import { Text } from "~/components/ui/Text";
+import { Text, View } from "tamagui";
export default function NotFoundScreen() {
return (
<>
-
-
- This screen doesn't exist.
-
+
+ This screen doesn't exist.
-
- Go to home screen!
+
+ Go to home screen!
>
diff --git a/apps/expo/src/app/_layout.tsx b/apps/expo/src/app/_layout.tsx
index dfbba6c..7c22070 100644
--- a/apps/expo/src/app/_layout.tsx
+++ b/apps/expo/src/app/_layout.tsx
@@ -1,26 +1,28 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { useEffect } from "react";
-import { useColorScheme } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
+// @ts-expect-error - No exported types
+import { ModalView } from "react-native-ios-modal";
import { useFonts } from "expo-font";
import { SplashScreen, Stack } from "expo-router";
import FontAwesome from "@expo/vector-icons/FontAwesome";
-import {
- DarkTheme,
- DefaultTheme,
- ThemeProvider,
-} from "@react-navigation/native";
+import { DarkTheme, ThemeProvider } from "@react-navigation/native";
+import { setupNativeSheet } from "@tamagui/sheet";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { TamaguiProvider, Theme, useTheme } from "tamagui";
+import tamaguiConfig from "tamagui.config";
-import "../styles/global.css";
-
-import { defaultTheme } from "@movie-web/tailwind-config/themes";
+import { useThemeStore } from "~/stores/theme";
+// @ts-expect-error - Without named import it causes an infinite loop
+import _styles from "../../tamagui-web.css";
export {
// Catch any errors thrown by the Layout component.
ErrorBoundary,
} from "expo-router";
+setupNativeSheet("ios", ModalView);
+
export const unstable_settings = {
// Ensure that reloading on `/modal` keeps a back button present.
initialRouteName: "(tabs)",
@@ -68,38 +70,50 @@ export default function RootLayout() {
);
}
+function ScreenStacks() {
+ const theme = useTheme();
+
+ return (
+
+
+
+ );
+}
+
function RootLayoutNav() {
- const colorScheme = useColorScheme();
+ const themeStore = useThemeStore((s) => s.theme);
return (
-
-
-
-
-
+
+
+
+
+
+
+
);
}
diff --git a/apps/expo/src/components/DownloadItem.tsx b/apps/expo/src/components/DownloadItem.tsx
index a3432de..95583e6 100644
--- a/apps/expo/src/components/DownloadItem.tsx
+++ b/apps/expo/src/components/DownloadItem.tsx
@@ -1,8 +1,5 @@
import React from "react";
-import { Text, View } from "react-native";
-import { Bar as ProgressBar } from "react-native-progress";
-
-import { defaultTheme } from "@movie-web/tailwind-config/themes";
+import { Progress, Text, View } from "tamagui";
export interface DownloadItemProps {
filename: string;
@@ -33,22 +30,28 @@ export const DownloadItem: React.FC = ({
const formattedDownloaded = formatBytes(downloaded);
return (
-
- {filename}
-
-
-
+
+
+ {filename}
+
+
+
+
{percentage}% - {formattedDownloaded} of {formattedFileSize}
- {speed} MB/s
+
+ {speed} MB/s
+
);
diff --git a/apps/expo/src/components/FlagIcon.tsx b/apps/expo/src/components/FlagIcon.tsx
new file mode 100644
index 0000000..466685c
--- /dev/null
+++ b/apps/expo/src/components/FlagIcon.tsx
@@ -0,0 +1,15 @@
+import { Image } from "tamagui";
+
+// TODO: Improve flag icons. This is incomplete.
+export function FlagIcon({ languageCode }: { languageCode: string }) {
+ return (
+
+ );
+}
diff --git a/apps/expo/src/components/HomeScreenContent.tsx b/apps/expo/src/components/HomeScreenContent.tsx
index f03f0e8..54c52ea 100644
--- a/apps/expo/src/components/HomeScreenContent.tsx
+++ b/apps/expo/src/components/HomeScreenContent.tsx
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react";
-import { Keyboard, ScrollView, View } from "react-native";
+import { Keyboard } from "react-native";
import Animated, {
Easing,
useAnimatedStyle,
@@ -7,6 +7,7 @@ import Animated, {
withTiming,
} from "react-native-reanimated";
import { useQuery } from "@tanstack/react-query";
+import { ScrollView, Text, View } from "tamagui";
import { getMediaPoster, searchTitle } from "@movie-web/tmdb";
@@ -19,7 +20,6 @@ import {
} from "~/components/item/ItemListSection";
import ScreenLayout from "~/components/layout/ScreenLayout";
import { SearchBar } from "~/components/ui/Searchbar";
-import { Text } from "~/components/ui/Text";
export default function HomeScreenContent() {
const [query, setQuery] = useState("");
@@ -103,7 +103,7 @@ export default function HomeScreenContent() {
};
return (
-
+
- Home
+
+
+ Home
+
}
>
{searchResultsLoaded ? (
-
+
{data?.map((item, index) => (
-
+
))}
diff --git a/apps/expo/src/components/Icon.tsx b/apps/expo/src/components/Icon.tsx
index 97568b1..dd942f5 100644
--- a/apps/expo/src/components/Icon.tsx
+++ b/apps/expo/src/components/Icon.tsx
@@ -1,11 +1,7 @@
import React from "react";
import Svg, { G, Path } from "react-native-svg";
-export const MovieWebSvg = ({
- fillColor = "currentColor",
-}: {
- fillColor?: string;
-}) => {
+export const MovieWebSvg = ({ fillColor }: { fillColor?: string }) => {
const svgPath =
"M18.186,4.5V6.241H16.445V4.5H9.482V6.241H7.741V4.5H6V20.168H7.741V18.427H9.482v1.741h6.964V18.427h1.741v1.741h1.741V4.5Zm-8.7,12.186H7.741V14.945H9.482Zm0-3.482H7.741V11.464H9.482Zm0-3.482H7.741V7.982H9.482Zm8.7,6.964H16.445V14.945h1.741Zm0-3.482H16.445V11.464h1.741Zm0-3.482H16.445V7.982h1.741Z";
diff --git a/apps/expo/src/components/SvgTabBarIcon.tsx b/apps/expo/src/components/SvgTabBarIcon.tsx
index 70a8a14..62de2f9 100644
--- a/apps/expo/src/components/SvgTabBarIcon.tsx
+++ b/apps/expo/src/components/SvgTabBarIcon.tsx
@@ -1,6 +1,6 @@
import React from "react";
-import { defaultTheme } from "@movie-web/tailwind-config/themes";
+import { useTheme } from "tamagui";
interface SvgTabBarIconProps {
focused?: boolean;
@@ -11,9 +11,8 @@ export default function SvgTabBarIcon({
focused,
children,
}: SvgTabBarIconProps) {
- const fillColor = focused
- ? defaultTheme.extend.colors.tabBar.active
- : defaultTheme.extend.colors.tabBar.inactive;
+ const theme = useTheme();
+ const fillColor = focused ? theme.tabBarIconFocused.val : theme.tabBarIcon.val;
if (React.isValidElement(children)) {
return React.cloneElement(children, { fillColor } as React.Attributes);
diff --git a/apps/expo/src/components/TabBarIcon.tsx b/apps/expo/src/components/TabBarIcon.tsx
index cd3f4cd..ad508dd 100644
--- a/apps/expo/src/components/TabBarIcon.tsx
+++ b/apps/expo/src/components/TabBarIcon.tsx
@@ -1,14 +1,13 @@
import { FontAwesome } from "@expo/vector-icons";
-import { defaultTheme } from "@movie-web/tailwind-config/themes";
+import { useTheme } from "tamagui";
type Props = {
focused?: boolean;
} & React.ComponentProps;
export default function TabBarIcon({ focused, ...rest }: Props) {
- const color = focused
- ? defaultTheme.extend.colors.tabBar.active
- : defaultTheme.extend.colors.tabBar.inactive;
+ const theme = useTheme();
+ const color = focused ? theme.tabBarIconFocused.val : theme.tabBarIcon.val
return ;
}
diff --git a/apps/expo/src/components/item/ItemListSection.tsx b/apps/expo/src/components/item/ItemListSection.tsx
index 9142778..ccde37b 100644
--- a/apps/expo/src/components/item/ItemListSection.tsx
+++ b/apps/expo/src/components/item/ItemListSection.tsx
@@ -1,5 +1,6 @@
import React from "react";
-import { Dimensions, ScrollView, Text, View } from "react-native";
+import { Dimensions } from "react-native";
+import { ScrollView, Text, View } from "tamagui";
import type { ItemData } from "~/components/item/item";
import Item from "~/components/item/item";
@@ -55,7 +56,7 @@ export const ItemListSection = ({
}) => {
return (
-
+
{title}
(
diff --git a/apps/expo/src/components/item/item.tsx b/apps/expo/src/components/item/item.tsx
index dc38906..427c3ad 100644
--- a/apps/expo/src/components/item/item.tsx
+++ b/apps/expo/src/components/item/item.tsx
@@ -1,10 +1,10 @@
import type { NativeSyntheticEvent } from "react-native";
import type { ContextMenuOnPressNativeEvent } from "react-native-context-menu-view";
-import { Image, Keyboard, TouchableOpacity, View } from "react-native";
+import { Keyboard, TouchableOpacity } from "react-native";
import ContextMenu from "react-native-context-menu-view";
import { useRouter } from "expo-router";
+import { Image, Text, View } from "tamagui";
-import { Text } from "~/components/ui/Text";
import { usePlayerStore } from "~/stores/player/store";
export interface ItemData {
@@ -47,19 +47,29 @@ export default function Item({ data }: { data: ItemData }) {
onLongPress={() => {}}
style={{ width: "100%" }}
>
-
+
-
-
+
+
- {title}
-
-
+
+ {title}
+
+
+
{type === "tv" ? "Show" : "Movie"}
-
- {year}
+
+
+ {year}
+
diff --git a/apps/expo/src/components/layout/ScreenLayout.tsx b/apps/expo/src/components/layout/ScreenLayout.tsx
index 339626d..d9745e3 100644
--- a/apps/expo/src/components/layout/ScreenLayout.tsx
+++ b/apps/expo/src/components/layout/ScreenLayout.tsx
@@ -1,6 +1,4 @@
-import { View } from "react-native";
-
-import { Text } from "~/components/ui/Text";
+import { Text, View } from "tamagui";
interface Props {
title?: React.ReactNode | string;
@@ -10,13 +8,17 @@ interface Props {
export default function ScreenLayout({ title, subtitle, children }: Props) {
return (
-
+
{typeof title === "string" && (
- {title}
+
+ {title}
+
)}
{typeof title !== "string" && title}
- {subtitle}
- {children}
+
+ {subtitle}
+
+ {children}
);
}
diff --git a/apps/expo/src/components/player/AudioTrackSelector.tsx b/apps/expo/src/components/player/AudioTrackSelector.tsx
index 8f4c44c..7097d39 100644
--- a/apps/expo/src/components/player/AudioTrackSelector.tsx
+++ b/apps/expo/src/components/player/AudioTrackSelector.tsx
@@ -1,17 +1,13 @@
-import { useEffect } from "react";
-import { Pressable, ScrollView, View } from "react-native";
-import Modal from "react-native-modal";
+import { useEffect, useState } from "react";
import { MaterialCommunityIcons } from "@expo/vector-icons";
-
-import { defaultTheme } from "@movie-web/tailwind-config/themes";
+import { useTheme } from "tamagui";
import { useAudioTrack } from "~/hooks/player/useAudioTrack";
-import { useBoolean } from "~/hooks/useBoolean";
import { useAudioTrackStore } from "~/stores/audio";
import { usePlayerStore } from "~/stores/player/store";
-import { Button } from "../ui/Button";
-import { Text } from "../ui/Text";
+import { MWButton } from "../ui/Button";
import { Controls } from "./Controls";
+import { Settings } from "./settings/Sheet";
export interface AudioTrack {
uri: string;
@@ -21,6 +17,9 @@ export interface AudioTrack {
}
export const AudioTrackSelector = () => {
+ const theme = useTheme();
+ const [open, setOpen] = useState(false);
+
const tracks = usePlayerStore((state) => state.interface.audioTracks);
const setAudioTracks = usePlayerStore((state) => state.setAudioTracks);
const stream = usePlayerStore((state) => state.interface.currentStream);
@@ -30,7 +29,6 @@ export const AudioTrackSelector = () => {
(state) => state.setSelectedAudioTrack,
);
- const { isTrue, on, off } = useBoolean();
const { synchronizePlayback } = useAudioTrack();
useEffect(() => {
@@ -52,58 +50,67 @@ export const AudioTrackSelector = () => {
if (!tracks?.length) return null;
return (
-
+ <>
-
}
- />
+ onPress={() => setOpen(true)}
+ >
+ Subtitles
+
-
-
- Select audio
- {tracks?.map((track) => (
- {
- setSelectedAudioTrack(track);
- if (stream) {
- void synchronizePlayback(track, stream);
+
+
+
+ setOpen(false)}
+ />
+ }
+ title="Audio"
+ />
+
+ {tracks?.map((track) => (
+
+ )
}
- off();
- }}
- >
- {track.name}
- {track.active && (
-
- )}
-
- ))}
-
-
-
+ onPress={() => {
+ setSelectedAudioTrack(track);
+ if (stream) {
+ void synchronizePlayback(track, stream);
+ }
+ }}
+ />
+ ))}
+
+
+
+ >
);
};
diff --git a/apps/expo/src/components/player/BackButton.tsx b/apps/expo/src/components/player/BackButton.tsx
index 51e5e39..3115697 100644
--- a/apps/expo/src/components/player/BackButton.tsx
+++ b/apps/expo/src/components/player/BackButton.tsx
@@ -4,9 +4,7 @@ import { Ionicons } from "@expo/vector-icons";
import { usePlayer } from "~/hooks/player/usePlayer";
-export const BackButton = ({
- className,
-}: Partial>) => {
+export const BackButton = () => {
const { dismissFullscreenPlayer } = usePlayer();
const router = useRouter();
@@ -30,7 +28,9 @@ export const BackButton = ({
}}
size={36}
color="white"
- className={className}
+ style={{
+ width: 100,
+ }}
/>
);
};
diff --git a/apps/expo/src/components/player/BottomControls.tsx b/apps/expo/src/components/player/BottomControls.tsx
index be6c49c..8afca83 100644
--- a/apps/expo/src/components/player/BottomControls.tsx
+++ b/apps/expo/src/components/player/BottomControls.tsx
@@ -1,8 +1,8 @@
import { useCallback, useMemo, useState } from "react";
-import { TouchableOpacity, View } from "react-native";
+import { TouchableOpacity } from "react-native";
+import { Text, View } from "tamagui";
import { usePlayerStore } from "~/stores/player/store";
-import { Text } from "../ui/Text";
import { AudioTrackSelector } from "./AudioTrackSelector";
import { CaptionsSelector } from "./CaptionsSelector";
import { Controls } from "./Controls";
@@ -44,13 +44,22 @@ export const BottomControls = () => {
if (status?.isLoaded) {
return (
-
+
-
- {currentTime}
- /
+
+ {currentTime}
+
+ /
+
-
+
{showRemaining ? remainingTime : durationTime}
@@ -58,7 +67,13 @@ export const BottomControls = () => {
-
+
diff --git a/apps/expo/src/components/player/CaptionRenderer.tsx b/apps/expo/src/components/player/CaptionRenderer.tsx
index 69c5607..f41b94c 100644
--- a/apps/expo/src/components/player/CaptionRenderer.tsx
+++ b/apps/expo/src/components/player/CaptionRenderer.tsx
@@ -1,5 +1,4 @@
import { useMemo } from "react";
-import { View } from "react-native";
import Animated, {
useAnimatedReaction,
useAnimatedStyle,
@@ -7,8 +6,8 @@ import Animated, {
useSharedValue,
withSpring,
} from "react-native-reanimated";
+import { Text, View } from "tamagui";
-import { Text } from "~/components/ui/Text";
import { convertMilliSecondsToSeconds } from "~/lib/number";
import { useCaptionsStore } from "~/stores/captions";
import { usePlayerStore } from "~/stores/player/store";
@@ -74,12 +73,21 @@ export const CaptionRenderer = () => {
return (
{visibleCaptions?.map((caption) => (
- {caption.text}
+ {caption.text}
))}
diff --git a/apps/expo/src/components/player/CaptionsSelector.tsx b/apps/expo/src/components/player/CaptionsSelector.tsx
index e272031..710e166 100644
--- a/apps/expo/src/components/player/CaptionsSelector.tsx
+++ b/apps/expo/src/components/player/CaptionsSelector.tsx
@@ -1,101 +1,153 @@
import type { ContentCaption } from "subsrt-ts/dist/types/handler";
-import { useCallback } from "react";
-import { Pressable, ScrollView, View } from "react-native";
-import Modal from "react-native-modal";
-import { MaterialCommunityIcons } from "@expo/vector-icons";
+import { useState } from "react";
+import { MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
+import { useMutation } from "@tanstack/react-query";
import { parse } from "subsrt-ts";
+import { useTheme, View } from "tamagui";
import type { Stream } from "@movie-web/provider-utils";
-import { defaultTheme } from "@movie-web/tailwind-config/themes";
-import { useBoolean } from "~/hooks/useBoolean";
+import type { CaptionWithData } from "~/stores/captions";
import { useCaptionsStore } from "~/stores/captions";
import { usePlayerStore } from "~/stores/player/store";
-import { Button } from "../ui/Button";
-import { Text } from "../ui/Text";
+import { FlagIcon } from "../FlagIcon";
+import { MWButton } from "../ui/Button";
import { Controls } from "./Controls";
+import { Settings } from "./settings/Sheet";
import { getPrettyLanguageNameFromLocale } from "./utils";
const parseCaption = async (
caption: Stream["captions"][0],
-): Promise => {
+): Promise => {
const response = await fetch(caption.url);
const data = await response.text();
- return parse(data).filter(
- (cue) => cue.type === "caption",
- ) as ContentCaption[];
+ return {
+ ...caption,
+ data: parse(data).filter(
+ (cue) => cue.type === "caption",
+ ) as ContentCaption[],
+ };
};
export const CaptionsSelector = () => {
+ const theme = useTheme();
+ const [open, setOpen] = useState(false);
const captions = usePlayerStore(
(state) => state.interface.currentStream?.captions,
);
+ const selectedCaption = useCaptionsStore((state) => state.selectedCaption);
const setSelectedCaption = useCaptionsStore(
(state) => state.setSelectedCaption,
);
- const { isTrue, on, off } = useBoolean();
- const downloadAndSetCaption = useCallback(
- (caption: Stream["captions"][0]) => {
- parseCaption(caption)
- .then((data) => {
- setSelectedCaption({ ...caption, data });
- })
- .catch(console.error);
+ const downloadCaption = useMutation({
+ mutationKey: ["captions", selectedCaption?.id],
+ mutationFn: parseCaption,
+ onSuccess: (data) => {
+ setSelectedCaption(data);
},
- [setSelectedCaption],
- );
+ });
if (!captions?.length) return null;
return (
-
+ <>
-
}
- />
+ onPress={() => setOpen(true)}
+ >
+ Subtitles
+
-
-
-
- Select subtitle
- {captions?.map((caption) => (
- {
- downloadAndSetCaption(caption);
- off();
- }}
- >
- {getPrettyLanguageNameFromLocale(caption.language)}
-
+
+
+ setOpen(false)}
/>
-
- ))}
-
-
-
+ }
+ title="Subtitles"
+ rightButton={
+
+ Customize
+
+ }
+ />
+
+
+ }
+ title={"Off"}
+ iconRight={
+ !selectedCaption?.id && (
+
+ )
+ }
+ onPress={() => setSelectedCaption(null)}
+ />
+
+ {captions?.map((caption) => (
+
+
+
+ }
+ title={getPrettyLanguageNameFromLocale(caption.language) ?? ""}
+ iconRight={
+ selectedCaption?.id === caption.id && (
+
+ )
+ }
+ onPress={() => downloadCaption.mutate(caption)}
+ key={caption.id}
+ />
+ ))}
+
+
+
+ >
);
};
diff --git a/apps/expo/src/components/player/ControlsOverlay.tsx b/apps/expo/src/components/player/ControlsOverlay.tsx
index a43ee33..981f47b 100644
--- a/apps/expo/src/components/player/ControlsOverlay.tsx
+++ b/apps/expo/src/components/player/ControlsOverlay.tsx
@@ -1,4 +1,4 @@
-import { View } from "react-native";
+import { View } from "tamagui";
import { BottomControls } from "./BottomControls";
import { Header } from "./Header";
@@ -6,7 +6,12 @@ import { MiddleControls } from "./MiddleControls";
export const ControlsOverlay = ({ isLoading }: { isLoading: boolean }) => {
return (
-
+
{!isLoading && }
diff --git a/apps/expo/src/components/player/Header.tsx b/apps/expo/src/components/player/Header.tsx
index d983aa7..621ec5b 100644
--- a/apps/expo/src/components/player/Header.tsx
+++ b/apps/expo/src/components/player/Header.tsx
@@ -1,8 +1,7 @@
-import { Image, View } from "react-native";
+import { Image, Text, View } from "tamagui";
import { usePlayerStore } from "~/stores/player/store";
import Icon from "../../../assets/images/icon-transparent.png";
-import { Text } from "../ui/Text";
import { BackButton } from "./BackButton";
import { Controls } from "./Controls";
@@ -16,11 +15,20 @@ export const Header = () => {
if (!isIdle && meta) {
return (
-
-
-
-
-
+
+
+
+
+
+
+
{meta.title} ({meta.releaseYear}){" "}
{meta.season !== undefined && meta.episode !== undefined
? mapSeasonAndEpisodeNumberToText(
@@ -29,9 +37,21 @@ export const Header = () => {
)
: ""}
-
-
- movie-web
+
+
+ movie-web
);
diff --git a/apps/expo/src/components/player/MiddleControls.tsx b/apps/expo/src/components/player/MiddleControls.tsx
index 3040d5d..01e8452 100644
--- a/apps/expo/src/components/player/MiddleControls.tsx
+++ b/apps/expo/src/components/player/MiddleControls.tsx
@@ -1,4 +1,5 @@
-import { StyleSheet, TouchableWithoutFeedback, View } from "react-native";
+import { TouchableWithoutFeedback } from "react-native";
+import { View } from "tamagui";
import { usePlayerStore } from "~/stores/player/store";
import { Controls } from "./Controls";
@@ -15,8 +16,17 @@ export const MiddleControls = () => {
return (
-
-
+
+
@@ -29,16 +39,3 @@ export const MiddleControls = () => {
);
};
-
-const styles = StyleSheet.create({
- container: {
- position: "absolute",
- height: "100%",
- width: "100%",
- flex: 1,
- flexDirection: "row",
- alignItems: "center",
- justifyContent: "center",
- gap: 82,
- },
-});
diff --git a/apps/expo/src/components/player/Modal.tsx b/apps/expo/src/components/player/Modal.tsx
new file mode 100644
index 0000000..a4fc05d
--- /dev/null
+++ b/apps/expo/src/components/player/Modal.tsx
@@ -0,0 +1,54 @@
+import { Button, Dialog, View } from "tamagui";
+
+import { Controls } from "./Controls";
+
+interface PlayerModalProps {
+ button: {
+ icon: JSX.Element;
+ title: string;
+ };
+ children?: React.ReactNode;
+}
+
+export function PlayerModal(props: PlayerModalProps) {
+ return (
+
+
+
+ );
+}
diff --git a/apps/expo/src/components/player/PlaybackSpeedSelector.tsx b/apps/expo/src/components/player/PlaybackSpeedSelector.tsx
index 7068b95..2553474 100644
--- a/apps/expo/src/components/player/PlaybackSpeedSelector.tsx
+++ b/apps/expo/src/components/player/PlaybackSpeedSelector.tsx
@@ -1,71 +1,82 @@
-import { Pressable, ScrollView, View } from "react-native";
-import Modal from "react-native-modal";
+import { useState } from "react";
import { MaterialCommunityIcons } from "@expo/vector-icons";
-
-import { defaultTheme } from "@movie-web/tailwind-config/themes";
+import { useTheme } from "tamagui";
import { usePlaybackSpeed } from "~/hooks/player/usePlaybackSpeed";
-import { useBoolean } from "~/hooks/useBoolean";
-import { Button } from "../ui/Button";
-import { Text } from "../ui/Text";
+import { MWButton } from "../ui/Button";
import { Controls } from "./Controls";
+import { Settings } from "./settings/Sheet";
export const PlaybackSpeedSelector = () => {
+ const theme = useTheme();
+ const [open, setOpen] = useState(false);
const { currentSpeed, changePlaybackSpeed } = usePlaybackSpeed();
- const { isTrue, on, off } = useBoolean();
const speeds = [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
return (
-
+ <>
-
}
- />
+ onPress={() => setOpen(true)}
+ >
+ Playback
+
-
-
- Select speed
- {speeds.map((speed) => (
- {
- changePlaybackSpeed(speed);
- off();
- }}
- >
- {speed}
- {speed === currentSpeed && (
-
- )}
-
- ))}
-
-
-
+
+
+
+ setOpen(false)}
+ />
+ }
+ title="Playback settings"
+ />
+
+ {speeds.map((speed) => (
+
+ )
+ }
+ onPress={() => {
+ changePlaybackSpeed(speed)
+ .then(() => setOpen(false))
+ .catch((err) => {
+ console.log("error", err);
+ });
+ }}
+ />
+ ))}
+
+
+
+ >
);
};
diff --git a/apps/expo/src/components/player/ProgressBar.tsx b/apps/expo/src/components/player/ProgressBar.tsx
index b3da177..373861b 100644
--- a/apps/expo/src/components/player/ProgressBar.tsx
+++ b/apps/expo/src/components/player/ProgressBar.tsx
@@ -21,7 +21,13 @@ export const ProgressBar = () => {
if (status?.isLoaded) {
return (
setIsIdle(false)}
>
diff --git a/apps/expo/src/components/player/ScrapeCard.tsx b/apps/expo/src/components/player/ScrapeCard.tsx
index 10eb181..e11a602 100644
--- a/apps/expo/src/components/player/ScrapeCard.tsx
+++ b/apps/expo/src/components/player/ScrapeCard.tsx
@@ -1,11 +1,7 @@
import type { ReactNode } from "react";
import React from "react";
-import { StyleSheet, View } from "react-native";
import { MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
-
-import { defaultTheme } from "@movie-web/tailwind-config/themes";
-
-import { Text } from "../ui/Text";
+import { Text, useTheme, View } from "tamagui";
export interface ScrapeItemProps {
status: "failure" | "pending" | "notfound" | "success" | "waiting";
@@ -37,41 +33,42 @@ export function StatusCircle({
type: ScrapeItemProps["status"];
percentage: number;
}) {
+ const theme = useTheme();
return (
<>
{type === "waiting" && (
)}
{type === "pending" && (
)}
{type === "failure" && (
)}
{type === "notfound" && (
)}
{type === "success" && (
)}
>
@@ -82,87 +79,37 @@ export function ScrapeItem(props: ScrapeItemProps) {
const text = statusTextMap[props.status];
return (
-
-
+
+
{props.name}
-
-
- {text && {text}}
+
+
+ {text && {text}}
- {props.children}
+ {props.children}
);
}
export function ScrapeCard(props: ScrapeCardProps) {
return (
-
+
);
}
-
-const styles = StyleSheet.create({
- scrapeItemContainer: {
- flex: 1,
- flexDirection: "column",
- },
- itemRow: {
- flexDirection: "row",
- alignItems: "center",
- gap: 16,
- },
- itemText: {
- fontSize: 18,
- },
- textPending: {
- color: "white",
- },
- textSecondary: {
- color: "secondaryColor",
- },
- textRow: {
- flexDirection: "row",
- gap: 16,
- },
- spacer: {
- width: 40,
- },
- statusText: {
- marginTop: 4,
- fontSize: 18,
- },
- childrenContainer: {
- marginLeft: 48,
- },
- cardContainer: {
- width: 384,
- },
- cardContent: {
- width: 384,
- borderRadius: 10,
- paddingVertical: 12,
- paddingHorizontal: 24,
- },
- cardBackground: {
- backgroundColor: "cardBackgroundColor",
- },
-});
diff --git a/apps/expo/src/components/player/ScraperProcess.tsx b/apps/expo/src/components/player/ScraperProcess.tsx
index 57d7b33..39375bf 100644
--- a/apps/expo/src/components/player/ScraperProcess.tsx
+++ b/apps/expo/src/components/player/ScraperProcess.tsx
@@ -1,7 +1,8 @@
import { useEffect, useRef } from "react";
-import { SafeAreaView, View } from "react-native";
+import { SafeAreaView } from "react-native";
import { ScrollView } from "react-native-gesture-handler";
import { useRouter } from "expo-router";
+import { View } from "tamagui";
import type { HlsBasedStream } from "@movie-web/provider-utils";
import { extractTracksFromHLS } from "@movie-web/provider-utils";
@@ -11,7 +12,6 @@ import type { AudioTrack } from "./AudioTrackSelector";
import { useMeta } from "~/hooks/player/useMeta";
import { useScrape } from "~/hooks/player/useSourceScrape";
import { constructFullUrl } from "~/lib/url";
-import { cn } from "~/lib/utils";
import { PlayerStatus } from "~/stores/player/slices/interface";
import { convertMetaToScrapeMedia } from "~/stores/player/slices/video";
import { usePlayerStore } from "~/stores/player/store";
@@ -106,17 +106,35 @@ export const ScraperProcess = ({ data }: ScraperProcessProps) => {
useEffect(() => {
scrollViewRef.current?.scrollTo({
- y: currentProviderIndex * 80,
+ y: currentProviderIndex * 110,
animated: true,
});
}, [currentProviderIndex]);
return (
-
-
+
+
{sourceOrder.map((order) => {
const source = sources[order.id];
@@ -138,9 +156,9 @@ export const ScraperProcess = ({ data }: ScraperProcessProps) => {
percentage={source.percentage}
>
0,
- })}
+ marginTop={order.children.length > 0 ? 8 : 0}
+ flexDirection="column"
+ gap={16}
>
{order.children.map((embedId) => {
const embed = sources[embedId];
diff --git a/apps/expo/src/components/player/SeasonEpisodeSelector.tsx b/apps/expo/src/components/player/SeasonEpisodeSelector.tsx
index 5aa4c2f..5de15fa 100644
--- a/apps/expo/src/components/player/SeasonEpisodeSelector.tsx
+++ b/apps/expo/src/components/player/SeasonEpisodeSelector.tsx
@@ -1,33 +1,25 @@
+import type { SheetProps } from "tamagui";
import { useState } from "react";
-import {
- ActivityIndicator,
- ScrollView,
- TouchableOpacity,
- View,
-} from "react-native";
-import Modal from "react-native-modal";
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import { useQuery } from "@tanstack/react-query";
+import { useTheme, View } from "tamagui";
-import { defaultTheme } from "@movie-web/tailwind-config/themes";
import { fetchMediaDetails, fetchSeasonDetails } from "@movie-web/tmdb";
-import { useBoolean } from "~/hooks/useBoolean";
import { usePlayerStore } from "~/stores/player/store";
-import { Button } from "../ui/Button";
-import { Divider } from "../ui/Divider";
-import { Text } from "../ui/Text";
+import { MWButton } from "../ui/Button";
import { Controls } from "./Controls";
+import { Settings } from "./settings/Sheet";
const EpisodeSelector = ({
seasonNumber,
setSelectedSeason,
- closeModal,
-}: {
+ ...props
+}: SheetProps & {
seasonNumber: number;
setSelectedSeason: (season: number | null) => void;
- closeModal: () => void;
}) => {
+ const theme = useTheme();
const meta = usePlayerStore((state) => state.meta);
const setMeta = usePlayerStore((state) => state.setMeta);
@@ -42,38 +34,47 @@ const EpisodeSelector = ({
if (!meta) return null;
return (
- <>
- {isLoading && (
-
-
-
- )}
- {data && (
-
-
+
+
+
+
+ setSelectedSeason(null)}
+ size={24}
+ color={theme.buttonSecondaryText.val}
+ onPress={() => {
+ setSelectedSeason(null);
+ props.onOpenChange?.(false);
+ }}
/>
-
- Season {data.season_number}
-
-
-
- {data.episodes.map((episode) => (
-
+
+ {data?.episodes.map((episode) => (
+
+
+ E{episode.episode_number}
+
+
+ }
+ title={episode.name}
onPress={() => {
setMeta({
...meta,
@@ -82,26 +83,23 @@ const EpisodeSelector = ({
tmdbId: episode.id.toString(),
},
});
- closeModal();
}}
- >
-
- E{episode.episode_number} {episode.name}
-
-
+ />
))}
-
- )}
- >
+
+
+
);
};
export const SeasonSelector = () => {
+ const theme = useTheme();
+ const [open, setOpen] = useState(false);
+ const [episodeOpen, setEpisodeOpen] = useState(false);
+
const [selectedSeason, setSelectedSeason] = useState(null);
const meta = usePlayerStore((state) => state.meta);
- const { isTrue, on, off } = useBoolean();
-
const { data, isLoading } = useQuery({
queryKey: ["seasons", meta!.tmdbId],
queryFn: async () => {
@@ -113,77 +111,74 @@ export const SeasonSelector = () => {
if (meta?.type !== "show") return null;
return (
-
+ <>
-
}
- />
+ onPress={() => setOpen(true)}
+ >
+ Episodes
+
-
- {selectedSeason === null && (
- <>
- {isLoading && (
-
-
-
- )}
- {data && (
-
-
- {data.result.name}
-
-
- {data.result.seasons.map((season) => (
-
+
+
+ {episodeOpen && selectedSeason ? (
+
+ ) : (
+ <>
+ setOpen(false)}
+ />
+ }
+ title={data?.result.name ?? ""}
+ />
+
+ {data?.result.seasons.map((season) => (
+ setSelectedSeason(season.season_number)}
- >
-
- Season {season.season_number}
-
-
-
+ title={`Season ${season.season_number}`}
+ iconRight={
+
+ }
+ onPress={() => {
+ setSelectedSeason(season.season_number);
+ setEpisodeOpen(true);
+ }}
+ />
))}
-
- )}
- >
- )}
- {selectedSeason !== null && (
-
- )}
-
-
+
+ >
+ )}
+
+
+ >
);
};
diff --git a/apps/expo/src/components/player/SourceSelector.tsx b/apps/expo/src/components/player/SourceSelector.tsx
index bf296be..e1e78d6 100644
--- a/apps/expo/src/components/player/SourceSelector.tsx
+++ b/apps/expo/src/components/player/SourceSelector.tsx
@@ -1,20 +1,18 @@
-import { useCallback, useState } from "react";
-import { ActivityIndicator, Pressable, ScrollView, View } from "react-native";
-import Modal from "react-native-modal";
+import type { SheetProps } from "tamagui";
+import { useCallback, useEffect, useState } from "react";
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
+import { Spinner, Text, useTheme, View } from "tamagui";
import { getBuiltinSources, providers } from "@movie-web/provider-utils";
-import { defaultTheme } from "@movie-web/tailwind-config/themes";
import {
useEmbedScrape,
useSourceScrape,
} from "~/hooks/player/useSourceScrape";
-import { useBoolean } from "~/hooks/useBoolean";
import { usePlayerStore } from "~/stores/player/store";
-import { Button } from "../ui/Button";
-import { Text } from "../ui/Text";
+import { MWButton } from "../ui/Button";
import { Controls } from "./Controls";
+import { Settings } from "./settings/Sheet";
const SourceItem = ({
name,
@@ -22,20 +20,39 @@ const SourceItem = ({
active,
embed,
onPress,
- closeModal,
}: {
name: string;
id: string;
active?: boolean;
embed?: { url: string; embedId: string };
onPress?: (id: string) => void;
- closeModal?: () => void;
}) => {
- const { mutate, isPending, isError } = useEmbedScrape(closeModal);
+ const theme = useTheme();
+ const { mutate, isPending, isError } = useEmbedScrape();
return (
-
+ {active && (
+
+ )}
+ {isError && (
+
+ )}
+
+ {isPending && }
+ >
+ }
onPress={() => {
if (onPress) {
onPress(id);
@@ -49,80 +66,86 @@ const SourceItem = ({
});
}
}}
- >
- {name}
- {active && (
-
- )}
- {isError && (
-
- )}
- {isPending && }
-
+ />
);
};
const EmbedsPart = ({
sourceId,
- setCurrentScreen,
- closeModal,
-}: {
+ closeParent,
+ ...props
+}: SheetProps & {
sourceId: string;
- setCurrentScreen: (screen: "source" | "embed") => void;
- closeModal: () => void;
+ closeParent?: (open: boolean) => void;
}) => {
- const { data, isPending, error } = useSourceScrape(sourceId, closeModal);
+ const theme = useTheme();
+ const { data, isPending, isError, error, status } = useSourceScrape(sourceId);
+
+ console.log(data);
+
+ useEffect(() => {
+ if (status === "success" && !isError && data && data?.length <= 1) {
+ props.onOpenChange?.(false);
+ closeParent?.(false);
+ }
+ }, [props.onOpenChange, status, data, isError]);
return (
-
-
- setCurrentScreen("source")}
+
+
+
+
+ {
+ props.onOpenChange?.(false);
+ }}
+ />
+ }
+ title={providers.getMetadata(sourceId)?.name ?? "Embeds"}
/>
- Embeds
-
- {isPending && }
- {error && {error.message}}
- {data && data?.length > 1 && (
-
- {data.map((embed) => {
- const metaData = providers.getMetadata(embed.embedId)!;
- return (
-
- );
- })}
-
- )}
-
+
+
+ {isPending && }
+ {error && Something went wrong!}
+
+ {data && data?.length > 1 && (
+
+ {data.map((embed) => {
+ const metaData = providers.getMetadata(embed.embedId)!;
+ return (
+
+ );
+ })}
+
+ )}
+
+
+
);
};
export const SourceSelector = () => {
- const [currentScreen, setCurrentScreen] = useState<"source" | "embed">(
- "source",
- );
+ const theme = useTheme();
+ const [open, setOpen] = useState(false);
+ const [embedOpen, setEmbedOpen] = useState(false);
+
const sourceId = usePlayerStore((state) => state.interface.sourceId);
const setSourceId = usePlayerStore((state) => state.setSourceId);
- const { isTrue, on, off } = useBoolean();
-
const isActive = useCallback(
(id: string) => {
return sourceId === id;
@@ -131,65 +154,71 @@ export const SourceSelector = () => {
);
return (
-
+ <>
-
}
- />
+ onPress={() => setOpen(true)}
+ >
+ Source
+
-
-
- {currentScreen === "source" && (
+
+
+
+ {embedOpen && sourceId ? (
+
+ ) : (
<>
- {getBuiltinSources()
- .sort((a, b) => b.rank - a.rank)
- .map((source) => (
- {
- setSourceId(source.id);
- setCurrentScreen("embed");
- }}
+ setOpen(false)}
/>
- ))}
+ }
+ title="Sources"
+ />
+
+ {getBuiltinSources()
+ .sort((a, b) => b.rank - a.rank)
+ .map((source) => (
+ {
+ setSourceId(id);
+ setEmbedOpen(true);
+ }}
+ />
+ ))}
+
>
)}
- {currentScreen === "embed" && (
-
- )}
-
-
-
+
+
+ >
);
};
diff --git a/apps/expo/src/components/player/StatusCircle.tsx b/apps/expo/src/components/player/StatusCircle.tsx
index eb76e9a..3c1163e 100644
--- a/apps/expo/src/components/player/StatusCircle.tsx
+++ b/apps/expo/src/components/player/StatusCircle.tsx
@@ -1,5 +1,4 @@
import React from "react";
-import { StyleSheet, View } from "react-native";
import Animated, {
Easing,
useAnimatedProps,
@@ -8,6 +7,7 @@ import Animated, {
} from "react-native-reanimated";
import { Circle, Svg } from "react-native-svg";
import { AntDesign } from "@expo/vector-icons";
+import { View } from "tamagui";
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
@@ -50,7 +50,7 @@ export const StatusCircle = ({
};
return (
-
+