mirror of
https://github.com/movie-web/native-app.git
synced 2025-09-13 18:13:25 +00:00
refactor to tamagui
This commit is contained in:
@@ -15,20 +15,19 @@ apps
|
|||||||
├─ Expo SDK 50
|
├─ Expo SDK 50
|
||||||
├─ React Native using React 18
|
├─ React Native using React 18
|
||||||
├─ Navigation using Expo Router
|
├─ Navigation using Expo Router
|
||||||
├─ Tailwind using Nativewind
|
└─ Styling with Tamagui
|
||||||
└─ Typesafe API calls using tRPC
|
|
||||||
packages
|
packages
|
||||||
├─ tmdb
|
├─ tmdb
|
||||||
| └─ Typesafe API calls to The Movie Database
|
| └─ Typesafe API calls to The Movie Database
|
||||||
└─ provider-utils
|
└─ provider-utils
|
||||||
└─ Typesafe API calls to the video providers
|
└─ Typesafe API calls to the video providers
|
||||||
tooling
|
tooling
|
||||||
|
├─ color
|
||||||
|
| └─ shared color palette
|
||||||
├─ eslint
|
├─ eslint
|
||||||
| └─ shared, fine-grained, eslint presets
|
| └─ shared, fine-grained, eslint presets
|
||||||
├─ prettier
|
├─ prettier
|
||||||
| └─ shared prettier configuration
|
| └─ shared prettier configuration
|
||||||
├─ tailwind
|
|
||||||
| └─ shared tailwind configuration
|
|
||||||
└─ typescript
|
└─ typescript
|
||||||
└─ shared tsconfig you can extend from
|
└─ shared tsconfig you can extend from
|
||||||
```
|
```
|
||||||
|
@@ -2,10 +2,7 @@
|
|||||||
module.exports = function (api) {
|
module.exports = function (api) {
|
||||||
api.cache(true);
|
api.cache(true);
|
||||||
return {
|
return {
|
||||||
presets: [
|
presets: ["babel-preset-expo"],
|
||||||
["babel-preset-expo", { jsxImportSource: "nativewind" }],
|
|
||||||
"nativewind/babel",
|
|
||||||
],
|
|
||||||
plugins: [
|
plugins: [
|
||||||
"react-native-reanimated/plugin",
|
"react-native-reanimated/plugin",
|
||||||
[
|
[
|
||||||
|
@@ -1,19 +1,19 @@
|
|||||||
// Learn more: https://docs.expo.dev/guides/monorepos/
|
// Learn more: https://docs.expo.dev/guides/monorepos/
|
||||||
const { getDefaultConfig } = require("expo/metro-config");
|
const { getDefaultConfig } = require("expo/metro-config");
|
||||||
const { FileStore } = require("metro-cache");
|
const { FileStore } = require("metro-cache");
|
||||||
const { withNativeWind } = require("nativewind/metro");
|
const { withTamagui } = require("@tamagui/metro-plugin");
|
||||||
|
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
module.exports = withTurborepoManagedCache(
|
module.exports = withTurborepoManagedCache(
|
||||||
withMonorepoPaths(
|
withMonorepoPaths(
|
||||||
withNativeWind(
|
withTamagui(
|
||||||
getDefaultConfig(__dirname, {
|
getDefaultConfig(__dirname, {
|
||||||
isCSSEnabled: true,
|
isCSSEnabled: true,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
input: "./src/styles/global.css",
|
components: ["tamagui"],
|
||||||
configPath: "./tailwind.config.ts",
|
config: "./tamagui.config.ts",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@@ -19,38 +19,42 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/metro-config": "^0.17.3",
|
"@expo/metro-config": "^0.17.3",
|
||||||
|
"@movie-web/colors": "*",
|
||||||
"@movie-web/provider-utils": "*",
|
"@movie-web/provider-utils": "*",
|
||||||
"@movie-web/tmdb": "*",
|
"@movie-web/tmdb": "*",
|
||||||
"@react-native-anywhere/polyfill-base64": "0.0.1-alpha.0",
|
"@react-native-anywhere/polyfill-base64": "0.0.1-alpha.0",
|
||||||
"@react-navigation/native": "^6.1.9",
|
"@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",
|
"@tanstack/react-query": "^5.22.2",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.0",
|
"expo": "~50.0.13",
|
||||||
"expo": "~50.0.5",
|
|
||||||
"expo-av": "~13.10.5",
|
"expo-av": "~13.10.5",
|
||||||
"expo-brightness": "~11.8.0",
|
"expo-brightness": "~11.8.0",
|
||||||
"expo-build-properties": "~0.11.1",
|
"expo-build-properties": "~0.11.1",
|
||||||
"expo-constants": "~15.4.5",
|
"expo-constants": "~15.4.5",
|
||||||
"expo-haptics": "~12.8.1",
|
"expo-haptics": "~12.8.1",
|
||||||
|
"expo-linear-gradient": "^12.7.2",
|
||||||
"expo-linking": "~6.2.2",
|
"expo-linking": "~6.2.2",
|
||||||
"expo-navigation-bar": "^2.8.1",
|
"expo-navigation-bar": "^2.8.1",
|
||||||
"expo-router": "~3.4.6",
|
"expo-router": "~3.4.8",
|
||||||
"expo-screen-orientation": "~6.4.1",
|
"expo-screen-orientation": "~6.4.1",
|
||||||
"expo-splash-screen": "~0.26.4",
|
"expo-splash-screen": "~0.26.4",
|
||||||
"expo-status-bar": "~1.11.1",
|
"expo-status-bar": "~1.11.1",
|
||||||
|
"expo-system-ui": "^2.9.3",
|
||||||
"expo-web-browser": "^12.8.2",
|
"expo-web-browser": "^12.8.2",
|
||||||
"immer": "^10.0.3",
|
"immer": "^10.0.3",
|
||||||
"iso-639-1": "^3.1.2",
|
"iso-639-1": "^3.1.2",
|
||||||
"nativewind": "^4.0.35",
|
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "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-context-menu-view": "^1.14.1",
|
||||||
"react-native-css-interop": "^0.0.35",
|
|
||||||
"react-native-gesture-handler": "~2.14.1",
|
"react-native-gesture-handler": "~2.14.1",
|
||||||
|
"react-native-ios-modal": "^0.1.8",
|
||||||
"react-native-modal": "^13.0.1",
|
"react-native-modal": "^13.0.1",
|
||||||
"react-native-paper": "^5.12.3",
|
"react-native-paper": "^5.12.3",
|
||||||
"react-native-progress": "^5.0.1",
|
|
||||||
"react-native-quick-base64": "^2.0.8",
|
"react-native-quick-base64": "^2.0.8",
|
||||||
"react-native-quick-crypto": "^0.6.1",
|
"react-native-quick-crypto": "^0.6.1",
|
||||||
"react-native-reanimated": "~3.6.2",
|
"react-native-reanimated": "~3.6.2",
|
||||||
@@ -60,7 +64,7 @@
|
|||||||
"react-native-url-polyfill": "^2.0.0",
|
"react-native-url-polyfill": "^2.0.0",
|
||||||
"react-native-web": "^0.19.10",
|
"react-native-web": "^0.19.10",
|
||||||
"subsrt-ts": "^2.1.2",
|
"subsrt-ts": "^2.1.2",
|
||||||
"tailwind-merge": "^2.2.1",
|
"tamagui": "^1.91.4",
|
||||||
"zustand": "^4.4.7"
|
"zustand": "^4.4.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -69,7 +73,6 @@
|
|||||||
"@babel/runtime": "^7.23.9",
|
"@babel/runtime": "^7.23.9",
|
||||||
"@movie-web/eslint-config": "workspace:^0.2.0",
|
"@movie-web/eslint-config": "workspace:^0.2.0",
|
||||||
"@movie-web/prettier-config": "workspace:^0.1.0",
|
"@movie-web/prettier-config": "workspace:^0.1.0",
|
||||||
"@movie-web/tailwind-config": "workspace:^0.1.0",
|
|
||||||
"@movie-web/tsconfig": "workspace:^0.1.0",
|
"@movie-web/tsconfig": "workspace:^0.1.0",
|
||||||
"@tanstack/eslint-plugin-query": "^5.20.1",
|
"@tanstack/eslint-plugin-query": "^5.20.1",
|
||||||
"@types/babel__core": "^7.20.5",
|
"@types/babel__core": "^7.20.5",
|
||||||
@@ -77,7 +80,6 @@
|
|||||||
"babel-plugin-module-resolver": "^5.0.0",
|
"babel-plugin-module-resolver": "^5.0.0",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"prettier": "^3.1.1",
|
"prettier": "^3.1.1",
|
||||||
"tailwindcss": "^3.4.0",
|
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
|
@@ -1,10 +1,9 @@
|
|||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { Platform, StyleSheet, View } from "react-native";
|
import { Platform } from "react-native";
|
||||||
import * as Haptics from "expo-haptics";
|
import * as Haptics from "expo-haptics";
|
||||||
import { Tabs } from "expo-router";
|
import { Tabs } from "expo-router";
|
||||||
import * as ScreenOrientation from "expo-screen-orientation";
|
import * as ScreenOrientation from "expo-screen-orientation";
|
||||||
|
import { useTheme, View } from "tamagui";
|
||||||
import { defaultTheme } from "@movie-web/tailwind-config/themes";
|
|
||||||
|
|
||||||
import { MovieWebSvg } from "~/components/Icon";
|
import { MovieWebSvg } from "~/components/Icon";
|
||||||
import SvgTabBarIcon from "~/components/SvgTabBarIcon";
|
import SvgTabBarIcon from "~/components/SvgTabBarIcon";
|
||||||
@@ -15,11 +14,13 @@ export default function TabLayout() {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
const focusSearchInputRef = useRef(() => {});
|
const focusSearchInputRef = useRef(() => {});
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SearchTabContext.Provider value={{ focusSearchInputRef }}>
|
<SearchTabContext.Provider value={{ focusSearchInputRef }}>
|
||||||
<Tabs
|
<Tabs
|
||||||
sceneContainerStyle={{
|
sceneContainerStyle={{
|
||||||
backgroundColor: defaultTheme.extend.colors.background.main,
|
backgroundColor: theme.screenBackground.val,
|
||||||
}}
|
}}
|
||||||
screenListeners={({ route }) => ({
|
screenListeners={({ route }) => ({
|
||||||
tabPress: () => {
|
tabPress: () => {
|
||||||
@@ -38,9 +39,9 @@ export default function TabLayout() {
|
|||||||
})}
|
})}
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
tabBarActiveTintColor: defaultTheme.extend.colors.tabBar.active,
|
tabBarActiveTintColor: theme.tabBarIconFocused.val,
|
||||||
tabBarStyle: {
|
tabBarStyle: {
|
||||||
backgroundColor: defaultTheme.extend.colors.tabBar.background,
|
backgroundColor: theme.tabBarBackground.val,
|
||||||
borderTopColor: "transparent",
|
borderTopColor: "transparent",
|
||||||
borderTopRightRadius: 20,
|
borderTopRightRadius: 20,
|
||||||
borderTopLeftRadius: 20,
|
borderTopLeftRadius: 20,
|
||||||
@@ -83,10 +84,16 @@ export default function TabLayout() {
|
|||||||
tabBarLabel: "",
|
tabBarLabel: "",
|
||||||
tabBarIcon: ({ focused }) => (
|
tabBarIcon: ({ focused }) => (
|
||||||
<View
|
<View
|
||||||
style={[
|
top={2}
|
||||||
styles.searchTab,
|
height={56}
|
||||||
focused ? styles.active : styles.inactive,
|
width={56}
|
||||||
]}
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
overflow="hidden"
|
||||||
|
borderRadius={100}
|
||||||
|
backgroundColor={
|
||||||
|
focused ? theme.tabBarIconFocused : theme.tabBarIcon
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<TabBarIcon name="search" color="#FFF" />
|
<TabBarIcon name="search" color="#FFF" />
|
||||||
</View>
|
</View>
|
||||||
@@ -117,22 +124,3 @@ export default function TabLayout() {
|
|||||||
</SearchTabContext.Provider>
|
</SearchTabContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { View } from "react-native";
|
import { Text, View } from "tamagui";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
bookmarks,
|
bookmarks,
|
||||||
@@ -7,15 +7,16 @@ import {
|
|||||||
watching,
|
watching,
|
||||||
} from "~/components/item/ItemListSection";
|
} from "~/components/item/ItemListSection";
|
||||||
import ScreenLayout from "~/components/layout/ScreenLayout";
|
import ScreenLayout from "~/components/layout/ScreenLayout";
|
||||||
import { Text } from "~/components/ui/Text";
|
|
||||||
|
|
||||||
export default function HomeScreen() {
|
export default function HomeScreen() {
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }} flex={1}>
|
||||||
<ScreenLayout
|
<ScreenLayout
|
||||||
title={
|
title={
|
||||||
<View className="flex-row items-center">
|
<View flexDirection="row" alignItems="center">
|
||||||
<Text className="text-2xl font-bold">Home</Text>
|
<Text fontWeight="bold" fontSize={20}>
|
||||||
|
Home
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
|
import { Text } from "tamagui";
|
||||||
|
|
||||||
import ScreenLayout from "~/components/layout/ScreenLayout";
|
import ScreenLayout from "~/components/layout/ScreenLayout";
|
||||||
import { Text } from "~/components/ui/Text";
|
|
||||||
|
|
||||||
export default function MovieWebScreen() {
|
export default function MovieWebScreen() {
|
||||||
return (
|
return (
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Keyboard, ScrollView, View } from "react-native";
|
import { Keyboard, ScrollView } from "react-native";
|
||||||
import Animated, {
|
import Animated, {
|
||||||
Easing,
|
Easing,
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
@@ -7,19 +7,14 @@ import Animated, {
|
|||||||
withTiming,
|
withTiming,
|
||||||
} from "react-native-reanimated";
|
} from "react-native-reanimated";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { Text, View } from "tamagui";
|
||||||
|
|
||||||
import { getMediaPoster, searchTitle } from "@movie-web/tmdb";
|
import { getMediaPoster, searchTitle } from "@movie-web/tmdb";
|
||||||
|
|
||||||
import type { ItemData } from "~/components/item/item";
|
import type { ItemData } from "~/components/item/item";
|
||||||
import Item 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 ScreenLayout from "~/components/layout/ScreenLayout";
|
||||||
import { SearchBar } from "~/components/ui/Searchbar";
|
import { SearchBar } from "~/components/ui/Searchbar";
|
||||||
import { Text } from "~/components/ui/Text";
|
|
||||||
|
|
||||||
export default function HomeScreen() {
|
export default function HomeScreen() {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
@@ -113,36 +108,28 @@ export default function HomeScreen() {
|
|||||||
>
|
>
|
||||||
<ScreenLayout
|
<ScreenLayout
|
||||||
title={
|
title={
|
||||||
<View className="flex-row items-center">
|
<View flexDirection="row" alignItems="center">
|
||||||
<Text className="text-2xl font-bold">Search</Text>
|
<Text fontWeight="bold" fontSize={20}>
|
||||||
|
Search
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{searchResultsLoaded ? (
|
{searchResultsLoaded && (
|
||||||
<Animated.View style={[searchResultsStyle, { flex: 1 }]}>
|
<Animated.View style={[searchResultsStyle, { flex: 1 }]}>
|
||||||
<View className="flex w-full flex-1 flex-row flex-wrap justify-start">
|
<View flexDirection="row" flexWrap="wrap">
|
||||||
{data?.map((item, index) => (
|
{data?.map((item, index) => (
|
||||||
<View key={index} className="basis-1/2 px-3 pb-3">
|
<View
|
||||||
|
key={index}
|
||||||
|
paddingHorizontal={12}
|
||||||
|
paddingBottom={12}
|
||||||
|
width="50%"
|
||||||
|
>
|
||||||
<Item data={item} />
|
<Item data={item} />
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
) : (
|
|
||||||
<ScrollView
|
|
||||||
scrollEnabled={
|
|
||||||
bookmarks.length > 0 || watching.length > 0 ? true : false
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ItemListSection
|
|
||||||
title="Bookmarks"
|
|
||||||
items={bookmarks.concat(watching)}
|
|
||||||
/>
|
|
||||||
<ItemListSection
|
|
||||||
title="Continue Watching"
|
|
||||||
items={watching.concat(bookmarks)}
|
|
||||||
/>
|
|
||||||
</ScrollView>
|
|
||||||
)}
|
)}
|
||||||
</ScreenLayout>
|
</ScreenLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
@@ -1,22 +1,142 @@
|
|||||||
import React, { useState } from "react";
|
import type { SelectProps } from "tamagui";
|
||||||
import { Text, View } from "react-native";
|
import React from "react";
|
||||||
import { Switch } from "react-native-paper";
|
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 ScreenLayout from "~/components/layout/ScreenLayout";
|
||||||
|
import { useThemeStore } from "~/stores/theme";
|
||||||
|
|
||||||
|
const themeOptions: ThemeStoreOption[] = [
|
||||||
|
"main",
|
||||||
|
"blue",
|
||||||
|
"gray",
|
||||||
|
"red",
|
||||||
|
"teal",
|
||||||
|
];
|
||||||
|
|
||||||
export default function SettingsScreen() {
|
export default function SettingsScreen() {
|
||||||
const [isSwitchOn, setIsSwitchOn] = useState(true);
|
|
||||||
const onToggleSwitch = () => setIsSwitchOn(!isSwitchOn);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScreenLayout title="Settings">
|
<ScreenLayout title="Settings">
|
||||||
<View className="p-4">
|
<View padding={4}>
|
||||||
<Text className="mb-4 text-lg font-bold text-white">Player</Text>
|
<Text marginBottom={4} fontSize={16} fontWeight="bold" color="white">
|
||||||
<View className="flex-row items-center justify-between rounded-lg border border-white px-4 py-2">
|
Player
|
||||||
<Text className="text-md text-white">Gesture Controls</Text>
|
</Text>
|
||||||
<Switch value={isSwitchOn} onValueChange={onToggleSwitch} />
|
<YStack>
|
||||||
</View>
|
<XStack width={200} alignItems="center" gap="$4">
|
||||||
|
<Label minWidth={110}>Gesture controls</Label>
|
||||||
|
<Separator minHeight={20} vertical />
|
||||||
|
<Switch size="$4" native>
|
||||||
|
<Switch.Thumb animation="quicker" />
|
||||||
|
</Switch>
|
||||||
|
</XStack>
|
||||||
|
|
||||||
|
<XStack width={200} alignItems="center" gap="$4">
|
||||||
|
<Label minWidth={110}>Theme</Label>
|
||||||
|
<Separator minHeight={20} vertical />
|
||||||
|
<ThemeSelector />
|
||||||
|
</XStack>
|
||||||
|
</YStack>
|
||||||
</View>
|
</View>
|
||||||
</ScreenLayout>
|
</ScreenLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ThemeSelector(props: SelectProps) {
|
||||||
|
const theme = useTheme();
|
||||||
|
const themeStore = useThemeStore((s) => s.theme);
|
||||||
|
const setTheme = useThemeStore((s) => s.setTheme);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
value={themeStore}
|
||||||
|
onValueChange={setTheme}
|
||||||
|
disablePreventBodyScroll
|
||||||
|
native
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Select.Trigger
|
||||||
|
maxWidth="$12"
|
||||||
|
iconAfter={<FontAwesome name="chevron-down" />}
|
||||||
|
>
|
||||||
|
<Select.Value />
|
||||||
|
</Select.Trigger>
|
||||||
|
|
||||||
|
<Adapt platform="native">
|
||||||
|
<Sheet
|
||||||
|
native
|
||||||
|
modal
|
||||||
|
dismissOnSnapToBottom
|
||||||
|
dismissOnOverlayPress
|
||||||
|
animationConfig={{
|
||||||
|
type: "spring",
|
||||||
|
damping: 20,
|
||||||
|
mass: 1.2,
|
||||||
|
stiffness: 250,
|
||||||
|
}}
|
||||||
|
snapPoints={[35]}
|
||||||
|
>
|
||||||
|
<Sheet.Handle backgroundColor="$sheetHandle" />
|
||||||
|
<Sheet.Frame backgroundColor="$sheetBackground" padding="$5">
|
||||||
|
<Adapt.Contents />
|
||||||
|
</Sheet.Frame>
|
||||||
|
<Sheet.Overlay
|
||||||
|
animation="lazy"
|
||||||
|
backgroundColor="rgba(0, 0, 0, 0.8)"
|
||||||
|
enterStyle={{ opacity: 0 }}
|
||||||
|
exitStyle={{ opacity: 0 }}
|
||||||
|
/>
|
||||||
|
</Sheet>
|
||||||
|
</Adapt>
|
||||||
|
|
||||||
|
<Select.Content>
|
||||||
|
<Select.Viewport
|
||||||
|
animation="static"
|
||||||
|
animateOnly={["transform", "opacity"]}
|
||||||
|
enterStyle={{ o: 0, y: -10 }}
|
||||||
|
exitStyle={{ o: 0, y: 10 }}
|
||||||
|
>
|
||||||
|
{themeOptions.map((item, i) => {
|
||||||
|
return (
|
||||||
|
<Select.Item
|
||||||
|
index={i}
|
||||||
|
key={item}
|
||||||
|
value={item}
|
||||||
|
backgroundColor="$sheetItemBackground"
|
||||||
|
borderTopRightRadius={i === 0 ? "$8" : 0}
|
||||||
|
borderTopLeftRadius={i === 0 ? "$8" : 0}
|
||||||
|
borderBottomRightRadius={
|
||||||
|
i === themeOptions.length - 1 ? "$8" : 0
|
||||||
|
}
|
||||||
|
borderBottomLeftRadius={
|
||||||
|
i === themeOptions.length - 1 ? "$8" : 0
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Select.ItemText>{item}</Select.ItemText>
|
||||||
|
<Select.ItemIndicator ml="auto">
|
||||||
|
<MaterialIcons
|
||||||
|
name="check-circle"
|
||||||
|
size={24}
|
||||||
|
color={theme.sheetItemSelected.val}
|
||||||
|
/>
|
||||||
|
</Select.ItemIndicator>
|
||||||
|
</Select.Item>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Select.Viewport>
|
||||||
|
</Select.Content>
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@@ -1,19 +1,21 @@
|
|||||||
import { View } from "react-native";
|
|
||||||
import { Link, Stack } from "expo-router";
|
import { Link, Stack } from "expo-router";
|
||||||
|
import { Text, View } from "tamagui";
|
||||||
import { Text } from "~/components/ui/Text";
|
|
||||||
|
|
||||||
export default function NotFoundScreen() {
|
export default function NotFoundScreen() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack.Screen options={{ title: "Oops!" }} />
|
<Stack.Screen options={{ title: "Oops!" }} />
|
||||||
<View className="flex-1 items-center justify-center p-5">
|
<View flex={1} alignItems="center" justifyContent="center" padding={5}>
|
||||||
<Text className="text-lg font-bold">
|
<Text fontWeight="bold">This screen doesn't exist.</Text>
|
||||||
This screen doesn't exist.
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Link href="/" className="mt-4 py-4">
|
<Link
|
||||||
<Text className="text-sm text-sky-500">Go to home screen!</Text>
|
href="/"
|
||||||
|
style={{
|
||||||
|
marginTop: 16,
|
||||||
|
paddingVertical: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text color="skyblue">Go to home screen!</Text>
|
||||||
</Link>
|
</Link>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
|
@@ -1,26 +1,28 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useColorScheme } from "react-native";
|
|
||||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
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 { useFonts } from "expo-font";
|
||||||
import { SplashScreen, Stack } from "expo-router";
|
import { SplashScreen, Stack } from "expo-router";
|
||||||
import FontAwesome from "@expo/vector-icons/FontAwesome";
|
import FontAwesome from "@expo/vector-icons/FontAwesome";
|
||||||
import {
|
import { DarkTheme, ThemeProvider } from "@react-navigation/native";
|
||||||
DarkTheme,
|
import { setupNativeSheet } from "@tamagui/sheet";
|
||||||
DefaultTheme,
|
|
||||||
ThemeProvider,
|
|
||||||
} from "@react-navigation/native";
|
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import { TamaguiProvider, Theme, useTheme } from "tamagui";
|
||||||
|
import tamaguiConfig from "tamagui.config";
|
||||||
|
|
||||||
import "../styles/global.css";
|
import { useThemeStore } from "~/stores/theme";
|
||||||
|
// @ts-expect-error - Without named import it causes an infinite loop
|
||||||
import { defaultTheme } from "@movie-web/tailwind-config/themes";
|
import _styles from "../../tamagui-web.css";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
// Catch any errors thrown by the Layout component.
|
// Catch any errors thrown by the Layout component.
|
||||||
ErrorBoundary,
|
ErrorBoundary,
|
||||||
} from "expo-router";
|
} from "expo-router";
|
||||||
|
|
||||||
|
setupNativeSheet("ios", ModalView);
|
||||||
|
|
||||||
export const unstable_settings = {
|
export const unstable_settings = {
|
||||||
// Ensure that reloading on `/modal` keeps a back button present.
|
// Ensure that reloading on `/modal` keeps a back button present.
|
||||||
initialRouteName: "(tabs)",
|
initialRouteName: "(tabs)",
|
||||||
@@ -68,12 +70,10 @@ export default function RootLayout() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RootLayoutNav() {
|
function ScreenStacks() {
|
||||||
const colorScheme = useColorScheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
|
|
||||||
<Stack
|
<Stack
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
autoHideHomeIndicator: true,
|
autoHideHomeIndicator: true,
|
||||||
@@ -83,7 +83,7 @@ function RootLayoutNav() {
|
|||||||
presentation: "card",
|
presentation: "card",
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
contentStyle: {
|
contentStyle: {
|
||||||
backgroundColor: defaultTheme.extend.colors.background.main,
|
backgroundColor: theme.screenBackground.val,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -99,7 +99,21 @@ function RootLayoutNav() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RootLayoutNav() {
|
||||||
|
const themeStore = useThemeStore((s) => s.theme);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<TamaguiProvider config={tamaguiConfig} defaultTheme="main">
|
||||||
|
<ThemeProvider value={DarkTheme}>
|
||||||
|
<Theme name={themeStore}>
|
||||||
|
<ScreenStacks />
|
||||||
|
</Theme>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
</TamaguiProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Text, View } from "react-native";
|
import { Progress, Text, View } from "tamagui";
|
||||||
import { Bar as ProgressBar } from "react-native-progress";
|
|
||||||
|
|
||||||
import { defaultTheme } from "@movie-web/tailwind-config/themes";
|
|
||||||
|
|
||||||
export interface DownloadItemProps {
|
export interface DownloadItemProps {
|
||||||
filename: string;
|
filename: string;
|
||||||
@@ -33,22 +30,28 @@ export const DownloadItem: React.FC<DownloadItemProps> = ({
|
|||||||
const formattedDownloaded = formatBytes(downloaded);
|
const formattedDownloaded = formatBytes(downloaded);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="mb-4 rounded-lg border border-white p-4">
|
<View marginBottom={16} borderRadius={8} borderColor="white" padding={16}>
|
||||||
<Text className="mb-2 text-lg text-white">{filename}</Text>
|
<Text marginBottom={4} fontSize={16}>
|
||||||
<ProgressBar
|
{filename}
|
||||||
progress={progress}
|
</Text>
|
||||||
width={null}
|
<Progress value={60} height={10} backgroundColor="$progressBackground">
|
||||||
color={defaultTheme.extend.colors.download.progressFilled}
|
<Progress.Indicator
|
||||||
unfilledColor={defaultTheme.extend.colors.download.progress}
|
animation="bounce"
|
||||||
borderWidth={0}
|
backgroundColor="$progressFilled"
|
||||||
height={10}
|
|
||||||
borderRadius={5}
|
|
||||||
/>
|
/>
|
||||||
<View className="mt-2 flex-row items-center justify-between">
|
</Progress>
|
||||||
<Text className="text-sm text-gray-600">
|
<View
|
||||||
|
marginTop={8}
|
||||||
|
flexDirection="row"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="space-between"
|
||||||
|
>
|
||||||
|
<Text fontSize={12} color="gray">
|
||||||
{percentage}% - {formattedDownloaded} of {formattedFileSize}
|
{percentage}% - {formattedDownloaded} of {formattedFileSize}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-sm text-gray-600">{speed} MB/s</Text>
|
<Text fontSize={12} color="gray">
|
||||||
|
{speed} MB/s
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
15
apps/expo/src/components/FlagIcon.tsx
Normal file
15
apps/expo/src/components/FlagIcon.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Image } from "tamagui";
|
||||||
|
|
||||||
|
// TODO: Improve flag icons. This is incomplete.
|
||||||
|
export function FlagIcon({ languageCode }: { languageCode: string }) {
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
source={{
|
||||||
|
uri: `https://flagcdn.com/w80/${languageCode.toLowerCase()}.png`,
|
||||||
|
}}
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
resizeMode="contain"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Keyboard, ScrollView, View } from "react-native";
|
import { Keyboard } from "react-native";
|
||||||
import Animated, {
|
import Animated, {
|
||||||
Easing,
|
Easing,
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
@@ -7,6 +7,7 @@ import Animated, {
|
|||||||
withTiming,
|
withTiming,
|
||||||
} from "react-native-reanimated";
|
} from "react-native-reanimated";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { ScrollView, Text, View } from "tamagui";
|
||||||
|
|
||||||
import { getMediaPoster, searchTitle } from "@movie-web/tmdb";
|
import { getMediaPoster, searchTitle } from "@movie-web/tmdb";
|
||||||
|
|
||||||
@@ -19,7 +20,6 @@ import {
|
|||||||
} from "~/components/item/ItemListSection";
|
} from "~/components/item/ItemListSection";
|
||||||
import ScreenLayout from "~/components/layout/ScreenLayout";
|
import ScreenLayout from "~/components/layout/ScreenLayout";
|
||||||
import { SearchBar } from "~/components/ui/Searchbar";
|
import { SearchBar } from "~/components/ui/Searchbar";
|
||||||
import { Text } from "~/components/ui/Text";
|
|
||||||
|
|
||||||
export default function HomeScreenContent() {
|
export default function HomeScreenContent() {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
@@ -103,7 +103,7 @@ export default function HomeScreenContent() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1 }}>
|
<View flex={1}>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
onScrollBeginDrag={handleScrollBegin}
|
onScrollBeginDrag={handleScrollBegin}
|
||||||
onMomentumScrollEnd={handleScrollEnd}
|
onMomentumScrollEnd={handleScrollEnd}
|
||||||
@@ -113,16 +113,23 @@ export default function HomeScreenContent() {
|
|||||||
>
|
>
|
||||||
<ScreenLayout
|
<ScreenLayout
|
||||||
title={
|
title={
|
||||||
<View className="flex-row items-center">
|
<View flexDirection="row" alignItems="center">
|
||||||
<Text className="text-2xl font-bold">Home</Text>
|
<Text fontWeight="bold" fontSize={20}>
|
||||||
|
Home
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{searchResultsLoaded ? (
|
{searchResultsLoaded ? (
|
||||||
<Animated.View style={[searchResultsStyle, { flex: 1 }]}>
|
<Animated.View style={[searchResultsStyle, { flex: 1 }]}>
|
||||||
<View className="flex w-full flex-1 flex-row flex-wrap justify-start">
|
<View
|
||||||
|
width="100%"
|
||||||
|
flexWrap="wrap"
|
||||||
|
justifyContent="flex-start"
|
||||||
|
flexDirection="row"
|
||||||
|
>
|
||||||
{data?.map((item, index) => (
|
{data?.map((item, index) => (
|
||||||
<View key={index} className="basis-1/2 px-3 pb-3">
|
<View key={index} flexBasis={1 / 2} paddingHorizontal={12}>
|
||||||
<Item data={item} />
|
<Item data={item} />
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
|
@@ -1,11 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import Svg, { G, Path } from "react-native-svg";
|
import Svg, { G, Path } from "react-native-svg";
|
||||||
|
|
||||||
export const MovieWebSvg = ({
|
export const MovieWebSvg = ({ fillColor }: { fillColor?: string }) => {
|
||||||
fillColor = "currentColor",
|
|
||||||
}: {
|
|
||||||
fillColor?: string;
|
|
||||||
}) => {
|
|
||||||
const svgPath =
|
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";
|
"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";
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { defaultTheme } from "@movie-web/tailwind-config/themes";
|
import { useTheme } from "tamagui";
|
||||||
|
|
||||||
interface SvgTabBarIconProps {
|
interface SvgTabBarIconProps {
|
||||||
focused?: boolean;
|
focused?: boolean;
|
||||||
@@ -11,9 +11,8 @@ export default function SvgTabBarIcon({
|
|||||||
focused,
|
focused,
|
||||||
children,
|
children,
|
||||||
}: SvgTabBarIconProps) {
|
}: SvgTabBarIconProps) {
|
||||||
const fillColor = focused
|
const theme = useTheme();
|
||||||
? defaultTheme.extend.colors.tabBar.active
|
const fillColor = focused ? theme.tabBarIconFocused.val : theme.tabBarIcon.val;
|
||||||
: defaultTheme.extend.colors.tabBar.inactive;
|
|
||||||
|
|
||||||
if (React.isValidElement(children)) {
|
if (React.isValidElement(children)) {
|
||||||
return React.cloneElement(children, { fillColor } as React.Attributes);
|
return React.cloneElement(children, { fillColor } as React.Attributes);
|
||||||
|
@@ -1,14 +1,13 @@
|
|||||||
import { FontAwesome } from "@expo/vector-icons";
|
import { FontAwesome } from "@expo/vector-icons";
|
||||||
|
|
||||||
import { defaultTheme } from "@movie-web/tailwind-config/themes";
|
import { useTheme } from "tamagui";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
focused?: boolean;
|
focused?: boolean;
|
||||||
} & React.ComponentProps<typeof FontAwesome>;
|
} & React.ComponentProps<typeof FontAwesome>;
|
||||||
|
|
||||||
export default function TabBarIcon({ focused, ...rest }: Props) {
|
export default function TabBarIcon({ focused, ...rest }: Props) {
|
||||||
const color = focused
|
const theme = useTheme();
|
||||||
? defaultTheme.extend.colors.tabBar.active
|
const color = focused ? theme.tabBarIconFocused.val : theme.tabBarIcon.val
|
||||||
: defaultTheme.extend.colors.tabBar.inactive;
|
|
||||||
return <FontAwesome color={color} size={24} {...rest} />;
|
return <FontAwesome color={color} size={24} {...rest} />;
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import React from "react";
|
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 type { ItemData } from "~/components/item/item";
|
||||||
import Item from "~/components/item/item";
|
import Item from "~/components/item/item";
|
||||||
@@ -55,7 +56,7 @@ export const ItemListSection = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<Text className="mb-2 mt-4 text-xl font-semibold text-white">
|
<Text marginBottom={8} marginTop={16} fontWeight="500" fontSize={20}>
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
@@ -66,11 +67,9 @@ export const ItemListSection = ({
|
|||||||
{items.map((item, index) => (
|
{items.map((item, index) => (
|
||||||
<View
|
<View
|
||||||
key={index}
|
key={index}
|
||||||
style={{
|
width={itemWidth}
|
||||||
width: itemWidth,
|
paddingHorizontal={padding / 2}
|
||||||
paddingHorizontal: padding / 2,
|
paddingBottom={padding}
|
||||||
paddingBottom: padding,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Item data={item} />
|
<Item data={item} />
|
||||||
</View>
|
</View>
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
import type { NativeSyntheticEvent } from "react-native";
|
import type { NativeSyntheticEvent } from "react-native";
|
||||||
import type { ContextMenuOnPressNativeEvent } from "react-native-context-menu-view";
|
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 ContextMenu from "react-native-context-menu-view";
|
||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
|
import { Image, Text, View } from "tamagui";
|
||||||
|
|
||||||
import { Text } from "~/components/ui/Text";
|
|
||||||
import { usePlayerStore } from "~/stores/player/store";
|
import { usePlayerStore } from "~/stores/player/store";
|
||||||
|
|
||||||
export interface ItemData {
|
export interface ItemData {
|
||||||
@@ -47,19 +47,29 @@ export default function Item({ data }: { data: ItemData }) {
|
|||||||
onLongPress={() => {}}
|
onLongPress={() => {}}
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
>
|
>
|
||||||
<View className="w-full">
|
<View width="100%">
|
||||||
<ContextMenu actions={contextMenuActions} onPress={onContextMenuPress}>
|
<ContextMenu actions={contextMenuActions} onPress={onContextMenuPress}>
|
||||||
<View className="mb-2 aspect-[9/14] w-full overflow-hidden rounded-2xl">
|
<View
|
||||||
<Image source={{ uri: posterUrl }} className="h-full w-full" />
|
marginBottom={4}
|
||||||
|
aspectRatio={9 / 14}
|
||||||
|
width="100%"
|
||||||
|
overflow="hidden"
|
||||||
|
borderRadius={24}
|
||||||
|
>
|
||||||
|
<Image source={{ uri: posterUrl }} width="100%" height="100%" />
|
||||||
</View>
|
</View>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
<Text className="font-bold">{title}</Text>
|
<Text fontWeight="bold" fontSize={14}>
|
||||||
<View className="flex-row items-center gap-3">
|
{title}
|
||||||
<Text className="text-xs text-gray-600">
|
</Text>
|
||||||
|
<View flexDirection="row" alignItems="center" gap={3}>
|
||||||
|
<Text fontSize={12} color="gray">
|
||||||
{type === "tv" ? "Show" : "Movie"}
|
{type === "tv" ? "Show" : "Movie"}
|
||||||
</Text>
|
</Text>
|
||||||
<View className="h-1 w-1 rounded-3xl bg-gray-600" />
|
<View height={1} width={1} borderRadius={24} backgroundColor="gray" />
|
||||||
<Text className="text-sm text-gray-600">{year}</Text>
|
<Text fontSize={12} color="gray">
|
||||||
|
{year}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
@@ -1,6 +1,4 @@
|
|||||||
import { View } from "react-native";
|
import { Text, View } from "tamagui";
|
||||||
|
|
||||||
import { Text } from "~/components/ui/Text";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title?: React.ReactNode | string;
|
title?: React.ReactNode | string;
|
||||||
@@ -10,13 +8,17 @@ interface Props {
|
|||||||
|
|
||||||
export default function ScreenLayout({ title, subtitle, children }: Props) {
|
export default function ScreenLayout({ title, subtitle, children }: Props) {
|
||||||
return (
|
return (
|
||||||
<View className="bg-shade-900 flex-1 p-12">
|
<View flex={1} padding={44} backgroundColor="$screenBackground">
|
||||||
{typeof title === "string" && (
|
{typeof title === "string" && (
|
||||||
<Text className="text-2xl font-bold">{title}</Text>
|
<Text fontWeight="bold" fontSize={24}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
)}
|
)}
|
||||||
{typeof title !== "string" && title}
|
{typeof title !== "string" && title}
|
||||||
<Text className="mt-1 text-sm font-bold">{subtitle}</Text>
|
<Text fontSize={16} fontWeight="bold" marginTop={1}>
|
||||||
<View className="py-3">{children}</View>
|
{subtitle}
|
||||||
|
</Text>
|
||||||
|
<View paddingVertical={12}>{children}</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,17 +1,13 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Pressable, ScrollView, View } from "react-native";
|
|
||||||
import Modal from "react-native-modal";
|
|
||||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||||
|
import { useTheme } from "tamagui";
|
||||||
import { defaultTheme } from "@movie-web/tailwind-config/themes";
|
|
||||||
|
|
||||||
import { useAudioTrack } from "~/hooks/player/useAudioTrack";
|
import { useAudioTrack } from "~/hooks/player/useAudioTrack";
|
||||||
import { useBoolean } from "~/hooks/useBoolean";
|
|
||||||
import { useAudioTrackStore } from "~/stores/audio";
|
import { useAudioTrackStore } from "~/stores/audio";
|
||||||
import { usePlayerStore } from "~/stores/player/store";
|
import { usePlayerStore } from "~/stores/player/store";
|
||||||
import { Button } from "../ui/Button";
|
import { MWButton } from "../ui/Button";
|
||||||
import { Text } from "../ui/Text";
|
|
||||||
import { Controls } from "./Controls";
|
import { Controls } from "./Controls";
|
||||||
|
import { Settings } from "./settings/Sheet";
|
||||||
|
|
||||||
export interface AudioTrack {
|
export interface AudioTrack {
|
||||||
uri: string;
|
uri: string;
|
||||||
@@ -21,6 +17,9 @@ export interface AudioTrack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const AudioTrackSelector = () => {
|
export const AudioTrackSelector = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const tracks = usePlayerStore((state) => state.interface.audioTracks);
|
const tracks = usePlayerStore((state) => state.interface.audioTracks);
|
||||||
const setAudioTracks = usePlayerStore((state) => state.setAudioTracks);
|
const setAudioTracks = usePlayerStore((state) => state.setAudioTracks);
|
||||||
const stream = usePlayerStore((state) => state.interface.currentStream);
|
const stream = usePlayerStore((state) => state.interface.currentStream);
|
||||||
@@ -30,7 +29,6 @@ export const AudioTrackSelector = () => {
|
|||||||
(state) => state.setSelectedAudioTrack,
|
(state) => state.setSelectedAudioTrack,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { isTrue, on, off } = useBoolean();
|
|
||||||
const { synchronizePlayback } = useAudioTrack();
|
const { synchronizePlayback } = useAudioTrack();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -52,58 +50,67 @@ export const AudioTrackSelector = () => {
|
|||||||
if (!tracks?.length) return null;
|
if (!tracks?.length) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="max-w-36 flex-1">
|
<>
|
||||||
<Controls>
|
<Controls>
|
||||||
<Button
|
<MWButton
|
||||||
title="Audio"
|
type="secondary"
|
||||||
variant="outline"
|
icon={
|
||||||
onPress={on}
|
|
||||||
iconLeft={
|
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name="volume-high"
|
name="subtitles"
|
||||||
size={24}
|
size={24}
|
||||||
color={defaultTheme.extend.colors.buttons.purple}
|
color={theme.buttonSecondaryText.val}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
onPress={() => setOpen(true)}
|
||||||
|
>
|
||||||
|
Subtitles
|
||||||
|
</MWButton>
|
||||||
</Controls>
|
</Controls>
|
||||||
|
|
||||||
<Modal
|
<Settings.Sheet
|
||||||
isVisible={isTrue}
|
forceRemoveScrollEnabled={open}
|
||||||
onBackdropPress={off}
|
open={open}
|
||||||
supportedOrientations={["portrait", "landscape"]}
|
onOpenChange={setOpen}
|
||||||
style={{
|
|
||||||
width: "35%",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignSelf: "center",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<ScrollView className="flex-1 bg-gray-900">
|
<Settings.SheetOverlay />
|
||||||
<Text className="text-center font-bold">Select audio</Text>
|
<Settings.SheetHandle />
|
||||||
|
<Settings.SheetFrame>
|
||||||
|
<Settings.Header
|
||||||
|
icon={
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name="close"
|
||||||
|
size={24}
|
||||||
|
color={theme.playerSettingsUnactiveText.val}
|
||||||
|
onPress={() => setOpen(false)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title="Audio"
|
||||||
|
/>
|
||||||
|
<Settings.Content>
|
||||||
{tracks?.map((track) => (
|
{tracks?.map((track) => (
|
||||||
<Pressable
|
<Settings.Item
|
||||||
className="flex w-full flex-row justify-between p-3"
|
|
||||||
key={track.language}
|
key={track.language}
|
||||||
|
title={track.name}
|
||||||
|
iconRight={
|
||||||
|
track.active && (
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name="check-circle"
|
||||||
|
size={24}
|
||||||
|
color={theme.playerSettingsUnactiveText.val}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setSelectedAudioTrack(track);
|
setSelectedAudioTrack(track);
|
||||||
if (stream) {
|
if (stream) {
|
||||||
void synchronizePlayback(track, stream);
|
void synchronizePlayback(track, stream);
|
||||||
}
|
}
|
||||||
off();
|
|
||||||
}}
|
}}
|
||||||
>
|
|
||||||
<Text>{track.name}</Text>
|
|
||||||
{track.active && (
|
|
||||||
<MaterialCommunityIcons
|
|
||||||
name="check-circle"
|
|
||||||
size={24}
|
|
||||||
color={defaultTheme.extend.colors.buttons.purple}
|
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</Pressable>
|
|
||||||
))}
|
))}
|
||||||
</ScrollView>
|
</Settings.Content>
|
||||||
</Modal>
|
</Settings.SheetFrame>
|
||||||
</View>
|
</Settings.Sheet>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -4,9 +4,7 @@ import { Ionicons } from "@expo/vector-icons";
|
|||||||
|
|
||||||
import { usePlayer } from "~/hooks/player/usePlayer";
|
import { usePlayer } from "~/hooks/player/usePlayer";
|
||||||
|
|
||||||
export const BackButton = ({
|
export const BackButton = () => {
|
||||||
className,
|
|
||||||
}: Partial<React.ComponentProps<typeof Ionicons>>) => {
|
|
||||||
const { dismissFullscreenPlayer } = usePlayer();
|
const { dismissFullscreenPlayer } = usePlayer();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -30,7 +28,9 @@ export const BackButton = ({
|
|||||||
}}
|
}}
|
||||||
size={36}
|
size={36}
|
||||||
color="white"
|
color="white"
|
||||||
className={className}
|
style={{
|
||||||
|
width: 100,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import { useCallback, useMemo, useState } from "react";
|
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 { usePlayerStore } from "~/stores/player/store";
|
||||||
import { Text } from "../ui/Text";
|
|
||||||
import { AudioTrackSelector } from "./AudioTrackSelector";
|
import { AudioTrackSelector } from "./AudioTrackSelector";
|
||||||
import { CaptionsSelector } from "./CaptionsSelector";
|
import { CaptionsSelector } from "./CaptionsSelector";
|
||||||
import { Controls } from "./Controls";
|
import { Controls } from "./Controls";
|
||||||
@@ -44,13 +44,22 @@ export const BottomControls = () => {
|
|||||||
|
|
||||||
if (status?.isLoaded) {
|
if (status?.isLoaded) {
|
||||||
return (
|
return (
|
||||||
<View className="flex h-32 w-full flex-col items-center justify-center p-6">
|
<View
|
||||||
|
height={128}
|
||||||
|
width="100%"
|
||||||
|
flexDirection="column"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
padding={24}
|
||||||
|
>
|
||||||
<Controls>
|
<Controls>
|
||||||
<View className="flex w-full flex-row items-center">
|
<View flexDirection="row" justifyContent="space-between" width="$11">
|
||||||
<Text className="font-bold">{currentTime}</Text>
|
<Text fontWeight="bold">{currentTime}</Text>
|
||||||
<Text className="mx-1 font-bold">/</Text>
|
<Text marginHorizontal={1} fontWeight="bold">
|
||||||
|
/
|
||||||
|
</Text>
|
||||||
<TouchableOpacity onPress={toggleTimeDisplay}>
|
<TouchableOpacity onPress={toggleTimeDisplay}>
|
||||||
<Text className="font-bold">
|
<Text fontWeight="bold">
|
||||||
{showRemaining ? remainingTime : durationTime}
|
{showRemaining ? remainingTime : durationTime}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@@ -58,7 +67,13 @@ export const BottomControls = () => {
|
|||||||
|
|
||||||
<ProgressBar />
|
<ProgressBar />
|
||||||
</Controls>
|
</Controls>
|
||||||
<View className="flex w-full flex-row items-center justify-center gap-4 pb-10">
|
<View
|
||||||
|
flexDirection="row"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
gap={4}
|
||||||
|
paddingBottom={40}
|
||||||
|
>
|
||||||
<SeasonSelector />
|
<SeasonSelector />
|
||||||
<CaptionsSelector />
|
<CaptionsSelector />
|
||||||
<SourceSelector />
|
<SourceSelector />
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { View } from "react-native";
|
|
||||||
import Animated, {
|
import Animated, {
|
||||||
useAnimatedReaction,
|
useAnimatedReaction,
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
@@ -7,8 +6,8 @@ import Animated, {
|
|||||||
useSharedValue,
|
useSharedValue,
|
||||||
withSpring,
|
withSpring,
|
||||||
} from "react-native-reanimated";
|
} from "react-native-reanimated";
|
||||||
|
import { Text, View } from "tamagui";
|
||||||
|
|
||||||
import { Text } from "~/components/ui/Text";
|
|
||||||
import { convertMilliSecondsToSeconds } from "~/lib/number";
|
import { convertMilliSecondsToSeconds } from "~/lib/number";
|
||||||
import { useCaptionsStore } from "~/stores/captions";
|
import { useCaptionsStore } from "~/stores/captions";
|
||||||
import { usePlayerStore } from "~/stores/player/store";
|
import { usePlayerStore } from "~/stores/player/store";
|
||||||
@@ -74,12 +73,21 @@ export const CaptionRenderer = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
className="absolute bottom-[95px] rounded bg-black/60 px-4 py-1 text-center leading-normal"
|
style={[
|
||||||
style={animatedStyles}
|
{
|
||||||
|
position: "absolute",
|
||||||
|
bottom: 95,
|
||||||
|
borderRadius: 4,
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.6)",
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 8,
|
||||||
|
},
|
||||||
|
animatedStyles,
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
{visibleCaptions?.map((caption) => (
|
{visibleCaptions?.map((caption) => (
|
||||||
<View key={caption.index}>
|
<View key={caption.index}>
|
||||||
<Text>{caption.text}</Text>
|
<Text style={{ textAlign: "center" }}>{caption.text}</Text>
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
@@ -1,101 +1,153 @@
|
|||||||
import type { ContentCaption } from "subsrt-ts/dist/types/handler";
|
import type { ContentCaption } from "subsrt-ts/dist/types/handler";
|
||||||
import { useCallback } from "react";
|
import { useState } from "react";
|
||||||
import { Pressable, ScrollView, View } from "react-native";
|
import { MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
|
||||||
import Modal from "react-native-modal";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
|
||||||
import { parse } from "subsrt-ts";
|
import { parse } from "subsrt-ts";
|
||||||
|
import { useTheme, View } from "tamagui";
|
||||||
|
|
||||||
import type { Stream } from "@movie-web/provider-utils";
|
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 { useCaptionsStore } from "~/stores/captions";
|
||||||
import { usePlayerStore } from "~/stores/player/store";
|
import { usePlayerStore } from "~/stores/player/store";
|
||||||
import { Button } from "../ui/Button";
|
import { FlagIcon } from "../FlagIcon";
|
||||||
import { Text } from "../ui/Text";
|
import { MWButton } from "../ui/Button";
|
||||||
import { Controls } from "./Controls";
|
import { Controls } from "./Controls";
|
||||||
|
import { Settings } from "./settings/Sheet";
|
||||||
import { getPrettyLanguageNameFromLocale } from "./utils";
|
import { getPrettyLanguageNameFromLocale } from "./utils";
|
||||||
|
|
||||||
const parseCaption = async (
|
const parseCaption = async (
|
||||||
caption: Stream["captions"][0],
|
caption: Stream["captions"][0],
|
||||||
): Promise<ContentCaption[]> => {
|
): Promise<CaptionWithData> => {
|
||||||
const response = await fetch(caption.url);
|
const response = await fetch(caption.url);
|
||||||
const data = await response.text();
|
const data = await response.text();
|
||||||
return parse(data).filter(
|
return {
|
||||||
|
...caption,
|
||||||
|
data: parse(data).filter(
|
||||||
(cue) => cue.type === "caption",
|
(cue) => cue.type === "caption",
|
||||||
) as ContentCaption[];
|
) as ContentCaption[],
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CaptionsSelector = () => {
|
export const CaptionsSelector = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
const captions = usePlayerStore(
|
const captions = usePlayerStore(
|
||||||
(state) => state.interface.currentStream?.captions,
|
(state) => state.interface.currentStream?.captions,
|
||||||
);
|
);
|
||||||
|
const selectedCaption = useCaptionsStore((state) => state.selectedCaption);
|
||||||
const setSelectedCaption = useCaptionsStore(
|
const setSelectedCaption = useCaptionsStore(
|
||||||
(state) => state.setSelectedCaption,
|
(state) => state.setSelectedCaption,
|
||||||
);
|
);
|
||||||
const { isTrue, on, off } = useBoolean();
|
|
||||||
|
|
||||||
const downloadAndSetCaption = useCallback(
|
const downloadCaption = useMutation({
|
||||||
(caption: Stream["captions"][0]) => {
|
mutationKey: ["captions", selectedCaption?.id],
|
||||||
parseCaption(caption)
|
mutationFn: parseCaption,
|
||||||
.then((data) => {
|
onSuccess: (data) => {
|
||||||
setSelectedCaption({ ...caption, data });
|
setSelectedCaption(data);
|
||||||
})
|
|
||||||
.catch(console.error);
|
|
||||||
},
|
},
|
||||||
[setSelectedCaption],
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (!captions?.length) return null;
|
if (!captions?.length) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="max-w-36 flex-1">
|
<>
|
||||||
<Controls>
|
<Controls>
|
||||||
<Button
|
<MWButton
|
||||||
title="Subtitles"
|
type="secondary"
|
||||||
variant="outline"
|
icon={
|
||||||
onPress={on}
|
|
||||||
iconLeft={
|
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name="subtitles"
|
name="subtitles"
|
||||||
size={24}
|
size={24}
|
||||||
color={defaultTheme.extend.colors.buttons.purple}
|
color={theme.buttonSecondaryText.val}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
onPress={() => setOpen(true)}
|
||||||
|
>
|
||||||
|
Subtitles
|
||||||
|
</MWButton>
|
||||||
</Controls>
|
</Controls>
|
||||||
|
<Settings.Sheet
|
||||||
<Modal
|
forceRemoveScrollEnabled={open}
|
||||||
isVisible={isTrue}
|
open={open}
|
||||||
onBackdropPress={off}
|
onOpenChange={setOpen}
|
||||||
supportedOrientations={["portrait", "landscape"]}
|
|
||||||
style={{
|
|
||||||
width: "35%",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignSelf: "center",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<ScrollView className="flex-1 bg-gray-900">
|
<Settings.SheetOverlay />
|
||||||
<Text className="text-center font-bold">Select subtitle</Text>
|
<Settings.SheetHandle />
|
||||||
{captions?.map((caption) => (
|
<Settings.SheetFrame>
|
||||||
<Pressable
|
<Settings.Header
|
||||||
className="flex w-full flex-row justify-between p-3"
|
icon={
|
||||||
key={caption.id}
|
<MaterialIcons
|
||||||
onPress={() => {
|
name="close"
|
||||||
downloadAndSetCaption(caption);
|
|
||||||
off();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text>{getPrettyLanguageNameFromLocale(caption.language)}</Text>
|
|
||||||
<MaterialCommunityIcons
|
|
||||||
name="download"
|
|
||||||
size={24}
|
size={24}
|
||||||
color={defaultTheme.extend.colors.buttons.purple}
|
color={theme.playerSettingsUnactiveText.val}
|
||||||
|
onPress={() => setOpen(false)}
|
||||||
/>
|
/>
|
||||||
</Pressable>
|
}
|
||||||
))}
|
title="Subtitles"
|
||||||
</ScrollView>
|
rightButton={
|
||||||
</Modal>
|
<MWButton
|
||||||
|
color="$playerSettingsUnactiveText"
|
||||||
|
fontWeight="bold"
|
||||||
|
chromeless
|
||||||
|
>
|
||||||
|
Customize
|
||||||
|
</MWButton>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Settings.Content>
|
||||||
|
<Settings.Item
|
||||||
|
iconLeft={
|
||||||
|
<View
|
||||||
|
width="$5"
|
||||||
|
height="$3"
|
||||||
|
backgroundColor="$subtitleSelectorBackground"
|
||||||
|
borderRadius="$5"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title={"Off"}
|
||||||
|
iconRight={
|
||||||
|
!selectedCaption?.id && (
|
||||||
|
<MaterialIcons
|
||||||
|
name="check-circle"
|
||||||
|
size={24}
|
||||||
|
color={theme.sheetItemSelected.val}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onPress={() => setSelectedCaption(null)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{captions?.map((caption) => (
|
||||||
|
<Settings.Item
|
||||||
|
iconLeft={
|
||||||
|
<View
|
||||||
|
width="$5"
|
||||||
|
height="$3"
|
||||||
|
backgroundColor="$subtitleSelectorBackground"
|
||||||
|
borderRadius="$5"
|
||||||
|
overflow="hidden"
|
||||||
|
>
|
||||||
|
<FlagIcon languageCode={caption.language} />
|
||||||
</View>
|
</View>
|
||||||
|
}
|
||||||
|
title={getPrettyLanguageNameFromLocale(caption.language) ?? ""}
|
||||||
|
iconRight={
|
||||||
|
selectedCaption?.id === caption.id && (
|
||||||
|
<MaterialIcons
|
||||||
|
name="check-circle"
|
||||||
|
size={24}
|
||||||
|
color={theme.sheetItemSelected.val}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onPress={() => downloadCaption.mutate(caption)}
|
||||||
|
key={caption.id}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Settings.Content>
|
||||||
|
</Settings.SheetFrame>
|
||||||
|
</Settings.Sheet>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { View } from "react-native";
|
import { View } from "tamagui";
|
||||||
|
|
||||||
import { BottomControls } from "./BottomControls";
|
import { BottomControls } from "./BottomControls";
|
||||||
import { Header } from "./Header";
|
import { Header } from "./Header";
|
||||||
@@ -6,7 +6,12 @@ import { MiddleControls } from "./MiddleControls";
|
|||||||
|
|
||||||
export const ControlsOverlay = ({ isLoading }: { isLoading: boolean }) => {
|
export const ControlsOverlay = ({ isLoading }: { isLoading: boolean }) => {
|
||||||
return (
|
return (
|
||||||
<View className="flex w-full flex-1 flex-col justify-between">
|
<View
|
||||||
|
width="100%"
|
||||||
|
flex={1}
|
||||||
|
flexDirection="column"
|
||||||
|
justifyContent="space-between"
|
||||||
|
>
|
||||||
<Header />
|
<Header />
|
||||||
{!isLoading && <MiddleControls />}
|
{!isLoading && <MiddleControls />}
|
||||||
<BottomControls />
|
<BottomControls />
|
||||||
|
@@ -1,8 +1,7 @@
|
|||||||
import { Image, View } from "react-native";
|
import { Image, Text, View } from "tamagui";
|
||||||
|
|
||||||
import { usePlayerStore } from "~/stores/player/store";
|
import { usePlayerStore } from "~/stores/player/store";
|
||||||
import Icon from "../../../assets/images/icon-transparent.png";
|
import Icon from "../../../assets/images/icon-transparent.png";
|
||||||
import { Text } from "../ui/Text";
|
|
||||||
import { BackButton } from "./BackButton";
|
import { BackButton } from "./BackButton";
|
||||||
import { Controls } from "./Controls";
|
import { Controls } from "./Controls";
|
||||||
|
|
||||||
@@ -16,11 +15,20 @@ export const Header = () => {
|
|||||||
|
|
||||||
if (!isIdle && meta) {
|
if (!isIdle && meta) {
|
||||||
return (
|
return (
|
||||||
<View className="z-50 flex h-16 w-full flex-row items-center justify-between px-6 pt-6">
|
<View
|
||||||
|
zIndex={50}
|
||||||
|
flexDirection="row"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="space-between"
|
||||||
|
height={64}
|
||||||
|
paddingHorizontal="$8"
|
||||||
|
>
|
||||||
|
<View width={144}>
|
||||||
<Controls>
|
<Controls>
|
||||||
<BackButton className="w-36" />
|
<BackButton />
|
||||||
</Controls>
|
</Controls>
|
||||||
<Text className="font-bold">
|
</View>
|
||||||
|
<Text fontWeight="bold">
|
||||||
{meta.title} ({meta.releaseYear}){" "}
|
{meta.title} ({meta.releaseYear}){" "}
|
||||||
{meta.season !== undefined && meta.episode !== undefined
|
{meta.season !== undefined && meta.episode !== undefined
|
||||||
? mapSeasonAndEpisodeNumberToText(
|
? mapSeasonAndEpisodeNumberToText(
|
||||||
@@ -29,9 +37,21 @@ export const Header = () => {
|
|||||||
)
|
)
|
||||||
: ""}
|
: ""}
|
||||||
</Text>
|
</Text>
|
||||||
<View className="flex h-12 w-36 flex-row items-center justify-center gap-2 space-x-2 rounded-full bg-pill-background px-4 py-2 opacity-80">
|
<View
|
||||||
<Image source={Icon} className="h-6 w-6" />
|
height={48}
|
||||||
<Text className="font-bold">movie-web</Text>
|
width={144}
|
||||||
|
flexDirection="row"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
gap={2}
|
||||||
|
paddingHorizontal={16}
|
||||||
|
paddingVertical={8}
|
||||||
|
opacity={0.8}
|
||||||
|
backgroundColor="$pillBackground"
|
||||||
|
borderRadius={24}
|
||||||
|
>
|
||||||
|
<Image source={Icon} height={24} width={24} />
|
||||||
|
<Text fontWeight="bold">movie-web</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
@@ -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 { usePlayerStore } from "~/stores/player/store";
|
||||||
import { Controls } from "./Controls";
|
import { Controls } from "./Controls";
|
||||||
@@ -15,8 +16,17 @@ export const MiddleControls = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableWithoutFeedback onPress={handleTouch}>
|
<TouchableWithoutFeedback onPress={handleTouch}>
|
||||||
<View style={styles.container}>
|
<View
|
||||||
<Controls className="mr-24">
|
position="absolute"
|
||||||
|
height="100%"
|
||||||
|
width="100%"
|
||||||
|
flex={1}
|
||||||
|
flexDirection="row"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
gap={82}
|
||||||
|
>
|
||||||
|
<Controls>
|
||||||
<SeekButton type="backward" />
|
<SeekButton type="backward" />
|
||||||
</Controls>
|
</Controls>
|
||||||
<Controls>
|
<Controls>
|
||||||
@@ -29,16 +39,3 @@ export const MiddleControls = () => {
|
|||||||
</TouchableWithoutFeedback>
|
</TouchableWithoutFeedback>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
position: "absolute",
|
|
||||||
height: "100%",
|
|
||||||
width: "100%",
|
|
||||||
flex: 1,
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
gap: 82,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
54
apps/expo/src/components/player/Modal.tsx
Normal file
54
apps/expo/src/components/player/Modal.tsx
Normal file
@@ -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 (
|
||||||
|
<View flex={1} maxWidth={144}>
|
||||||
|
<Dialog modal>
|
||||||
|
<Dialog.Trigger asChild>
|
||||||
|
<Controls>
|
||||||
|
<Button icon={props.button.icon}>{props.button.title}</Button>
|
||||||
|
</Controls>
|
||||||
|
</Dialog.Trigger>
|
||||||
|
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Overlay
|
||||||
|
key="overlay"
|
||||||
|
animation="slow"
|
||||||
|
opacity={0.5}
|
||||||
|
enterStyle={{ opacity: 0 }}
|
||||||
|
exitStyle={{ opacity: 0 }}
|
||||||
|
/>
|
||||||
|
<Dialog.Content
|
||||||
|
bordered
|
||||||
|
elevate
|
||||||
|
key="content"
|
||||||
|
animateOnly={["transform", "opacity"]}
|
||||||
|
animation={[
|
||||||
|
"quicker",
|
||||||
|
{
|
||||||
|
opacity: {
|
||||||
|
overshootClamping: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
enterStyle={{ x: 0, y: -20, opacity: 0, scale: 0.9 }}
|
||||||
|
exitStyle={{ x: 0, y: 10, opacity: 0, scale: 0.95 }}
|
||||||
|
gap="$4"
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
@@ -1,71 +1,82 @@
|
|||||||
import { Pressable, ScrollView, View } from "react-native";
|
import { useState } from "react";
|
||||||
import Modal from "react-native-modal";
|
|
||||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||||
|
import { useTheme } from "tamagui";
|
||||||
import { defaultTheme } from "@movie-web/tailwind-config/themes";
|
|
||||||
|
|
||||||
import { usePlaybackSpeed } from "~/hooks/player/usePlaybackSpeed";
|
import { usePlaybackSpeed } from "~/hooks/player/usePlaybackSpeed";
|
||||||
import { useBoolean } from "~/hooks/useBoolean";
|
import { MWButton } from "../ui/Button";
|
||||||
import { Button } from "../ui/Button";
|
|
||||||
import { Text } from "../ui/Text";
|
|
||||||
import { Controls } from "./Controls";
|
import { Controls } from "./Controls";
|
||||||
|
import { Settings } from "./settings/Sheet";
|
||||||
|
|
||||||
export const PlaybackSpeedSelector = () => {
|
export const PlaybackSpeedSelector = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
const { currentSpeed, changePlaybackSpeed } = usePlaybackSpeed();
|
const { currentSpeed, changePlaybackSpeed } = usePlaybackSpeed();
|
||||||
const { isTrue, on, off } = useBoolean();
|
|
||||||
|
|
||||||
const speeds = [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
|
const speeds = [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="max-w-36 flex-1">
|
<>
|
||||||
<Controls>
|
<Controls>
|
||||||
<Button
|
<MWButton
|
||||||
title="Speed"
|
type="secondary"
|
||||||
variant="outline"
|
icon={
|
||||||
onPress={on}
|
|
||||||
iconLeft={
|
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name="speedometer"
|
name="speedometer"
|
||||||
size={24}
|
size={24}
|
||||||
color={defaultTheme.extend.colors.buttons.purple}
|
color={theme.buttonSecondaryText.val}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
onPress={() => setOpen(true)}
|
||||||
|
>
|
||||||
|
Playback
|
||||||
|
</MWButton>
|
||||||
</Controls>
|
</Controls>
|
||||||
|
|
||||||
<Modal
|
<Settings.Sheet
|
||||||
isVisible={isTrue}
|
forceRemoveScrollEnabled={open}
|
||||||
onBackdropPress={off}
|
open={open}
|
||||||
supportedOrientations={["portrait", "landscape"]}
|
onOpenChange={setOpen}
|
||||||
style={{
|
|
||||||
width: "35%",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignSelf: "center",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<ScrollView className="flex-1 bg-gray-900">
|
<Settings.SheetOverlay />
|
||||||
<Text className="text-center font-bold">Select speed</Text>
|
<Settings.SheetHandle />
|
||||||
|
<Settings.SheetFrame>
|
||||||
|
<Settings.Header
|
||||||
|
icon={
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name="close"
|
||||||
|
size={24}
|
||||||
|
color={theme.playerSettingsUnactiveText.val}
|
||||||
|
onPress={() => setOpen(false)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title="Playback settings"
|
||||||
|
/>
|
||||||
|
<Settings.Content>
|
||||||
{speeds.map((speed) => (
|
{speeds.map((speed) => (
|
||||||
<Pressable
|
<Settings.Item
|
||||||
className="flex w-full flex-row justify-between p-3"
|
|
||||||
key={speed}
|
key={speed}
|
||||||
onPress={() => {
|
title={`${speed}x`}
|
||||||
changePlaybackSpeed(speed);
|
iconRight={
|
||||||
off();
|
speed === currentSpeed && (
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text>{speed}</Text>
|
|
||||||
{speed === currentSpeed && (
|
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name="check-circle"
|
name="check-circle"
|
||||||
size={24}
|
size={24}
|
||||||
color={defaultTheme.extend.colors.buttons.purple}
|
color={theme.sheetItemSelected.val}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onPress={() => {
|
||||||
|
changePlaybackSpeed(speed)
|
||||||
|
.then(() => setOpen(false))
|
||||||
|
.catch((err) => {
|
||||||
|
console.log("error", err);
|
||||||
|
});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</Pressable>
|
|
||||||
))}
|
))}
|
||||||
</ScrollView>
|
</Settings.Content>
|
||||||
</Modal>
|
</Settings.SheetFrame>
|
||||||
</View>
|
</Settings.Sheet>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -21,7 +21,13 @@ export const ProgressBar = () => {
|
|||||||
if (status?.isLoaded) {
|
if (status?.isLoaded) {
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
className="flex flex-1 items-center justify-center pb-8 pt-6"
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
paddingBottom: 36,
|
||||||
|
paddingTop: 24,
|
||||||
|
}}
|
||||||
onPress={() => setIsIdle(false)}
|
onPress={() => setIsIdle(false)}
|
||||||
>
|
>
|
||||||
<VideoSlider onSlidingComplete={updateProgress} />
|
<VideoSlider onSlidingComplete={updateProgress} />
|
||||||
|
@@ -1,11 +1,7 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { StyleSheet, View } from "react-native";
|
|
||||||
import { MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
|
import { MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
|
||||||
|
import { Text, useTheme, View } from "tamagui";
|
||||||
import { defaultTheme } from "@movie-web/tailwind-config/themes";
|
|
||||||
|
|
||||||
import { Text } from "../ui/Text";
|
|
||||||
|
|
||||||
export interface ScrapeItemProps {
|
export interface ScrapeItemProps {
|
||||||
status: "failure" | "pending" | "notfound" | "success" | "waiting";
|
status: "failure" | "pending" | "notfound" | "success" | "waiting";
|
||||||
@@ -37,41 +33,42 @@ export function StatusCircle({
|
|||||||
type: ScrapeItemProps["status"];
|
type: ScrapeItemProps["status"];
|
||||||
percentage: number;
|
percentage: number;
|
||||||
}) {
|
}) {
|
||||||
|
const theme = useTheme();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{type === "waiting" && (
|
{type === "waiting" && (
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name="circle-outline"
|
name="circle-outline"
|
||||||
size={40}
|
size={40}
|
||||||
color={defaultTheme.extend.colors.video.scraping.noresult}
|
color={theme.scrapingNoResult.val}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{type === "pending" && (
|
{type === "pending" && (
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name={mapPercentageToIcon(percentage) as "circle-slice-1"}
|
name={mapPercentageToIcon(percentage) as "circle-slice-1"}
|
||||||
size={40}
|
size={40}
|
||||||
color={defaultTheme.extend.colors.video.scraping.loading}
|
color={theme.scrapingLoading.val}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{type === "failure" && (
|
{type === "failure" && (
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name="close-circle"
|
name="close-circle"
|
||||||
size={40}
|
size={40}
|
||||||
color={defaultTheme.extend.colors.video.scraping.error}
|
color={theme.scrapingError.val}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{type === "notfound" && (
|
{type === "notfound" && (
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name="remove-circle"
|
name="remove-circle"
|
||||||
size={40}
|
size={40}
|
||||||
color={defaultTheme.extend.colors.video.scraping.noresult}
|
color={theme.scrapingNoResult.val}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{type === "success" && (
|
{type === "success" && (
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name="check-circle"
|
name="check-circle"
|
||||||
size={40}
|
size={40}
|
||||||
color={defaultTheme.extend.colors.video.scraping.success}
|
color={theme.scrapingSuccess.val}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -82,87 +79,37 @@ export function ScrapeItem(props: ScrapeItemProps) {
|
|||||||
const text = statusTextMap[props.status];
|
const text = statusTextMap[props.status];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.scrapeItemContainer}>
|
<View flex={1} flexDirection="column">
|
||||||
<View style={styles.itemRow}>
|
<View flexDirection="row" alignItems="center" gap={16}>
|
||||||
<StatusCircle type={props.status} percentage={props.percentage ?? 0} />
|
<StatusCircle type={props.status} percentage={props.percentage ?? 0} />
|
||||||
<Text
|
<Text
|
||||||
style={[
|
fontSize={18}
|
||||||
styles.itemText,
|
color={props.status === "pending" ? "$scrapingLoading" : "white"}
|
||||||
props.status === "pending"
|
|
||||||
? styles.textPending
|
|
||||||
: styles.textSecondary,
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
{props.name}
|
{props.name}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.textRow}>
|
<View flexDirection="row" alignItems="center" gap={16}>
|
||||||
<View style={styles.spacer} />
|
<View width={40} />
|
||||||
<View>{text && <Text style={styles.statusText}>{text}</Text>}</View>
|
<View>{text && <Text fontSize={18}>{text}</Text>}</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.childrenContainer}>{props.children}</View>
|
<View marginLeft={48}>{props.children}</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ScrapeCard(props: ScrapeCardProps) {
|
export function ScrapeCard(props: ScrapeCardProps) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.cardContainer}>
|
<View width={384}>
|
||||||
<View
|
<View
|
||||||
style={[
|
width="100%"
|
||||||
styles.cardContent,
|
borderRadius={10}
|
||||||
props.hasChildren ? styles.cardBackground : null,
|
paddingVertical={12}
|
||||||
]}
|
paddingHorizontal={24}
|
||||||
|
backgroundColor={props.hasChildren ? "$scrapingCard" : "transparent"}
|
||||||
>
|
>
|
||||||
<ScrapeItem {...props} />
|
<ScrapeItem {...props} />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { SafeAreaView, View } from "react-native";
|
import { SafeAreaView } from "react-native";
|
||||||
import { ScrollView } from "react-native-gesture-handler";
|
import { ScrollView } from "react-native-gesture-handler";
|
||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
|
import { View } from "tamagui";
|
||||||
|
|
||||||
import type { HlsBasedStream } from "@movie-web/provider-utils";
|
import type { HlsBasedStream } from "@movie-web/provider-utils";
|
||||||
import { extractTracksFromHLS } 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 { useMeta } from "~/hooks/player/useMeta";
|
||||||
import { useScrape } from "~/hooks/player/useSourceScrape";
|
import { useScrape } from "~/hooks/player/useSourceScrape";
|
||||||
import { constructFullUrl } from "~/lib/url";
|
import { constructFullUrl } from "~/lib/url";
|
||||||
import { cn } from "~/lib/utils";
|
|
||||||
import { PlayerStatus } from "~/stores/player/slices/interface";
|
import { PlayerStatus } from "~/stores/player/slices/interface";
|
||||||
import { convertMetaToScrapeMedia } from "~/stores/player/slices/video";
|
import { convertMetaToScrapeMedia } from "~/stores/player/slices/video";
|
||||||
import { usePlayerStore } from "~/stores/player/store";
|
import { usePlayerStore } from "~/stores/player/store";
|
||||||
@@ -106,17 +106,35 @@ export const ScraperProcess = ({ data }: ScraperProcessProps) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollViewRef.current?.scrollTo({
|
scrollViewRef.current?.scrollTo({
|
||||||
y: currentProviderIndex * 80,
|
y: currentProviderIndex * 110,
|
||||||
animated: true,
|
animated: true,
|
||||||
});
|
});
|
||||||
}, [currentProviderIndex]);
|
}, [currentProviderIndex]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView className="flex h-full flex-1 flex-col">
|
<SafeAreaView
|
||||||
<View className="flex-1 items-center justify-center bg-background-main">
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
height: "100%",
|
||||||
|
flexDirection: "column",
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
flex={1}
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
backgroundColor="$screenBackground"
|
||||||
|
>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
ref={scrollViewRef}
|
ref={scrollViewRef}
|
||||||
contentContainerClassName="items-center flex flex-col py-16"
|
contentContainerStyle={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
paddingVertical: 64,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{sourceOrder.map((order) => {
|
{sourceOrder.map((order) => {
|
||||||
const source = sources[order.id];
|
const source = sources[order.id];
|
||||||
@@ -138,9 +156,9 @@ export const ScraperProcess = ({ data }: ScraperProcessProps) => {
|
|||||||
percentage={source.percentage}
|
percentage={source.percentage}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
className={cn({
|
marginTop={order.children.length > 0 ? 8 : 0}
|
||||||
"mt-8 space-y-6": order.children.length > 0,
|
flexDirection="column"
|
||||||
})}
|
gap={16}
|
||||||
>
|
>
|
||||||
{order.children.map((embedId) => {
|
{order.children.map((embedId) => {
|
||||||
const embed = sources[embedId];
|
const embed = sources[embedId];
|
||||||
|
@@ -1,33 +1,25 @@
|
|||||||
|
import type { SheetProps } from "tamagui";
|
||||||
import { useState } from "react";
|
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 { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
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 { fetchMediaDetails, fetchSeasonDetails } from "@movie-web/tmdb";
|
||||||
|
|
||||||
import { useBoolean } from "~/hooks/useBoolean";
|
|
||||||
import { usePlayerStore } from "~/stores/player/store";
|
import { usePlayerStore } from "~/stores/player/store";
|
||||||
import { Button } from "../ui/Button";
|
import { MWButton } from "../ui/Button";
|
||||||
import { Divider } from "../ui/Divider";
|
|
||||||
import { Text } from "../ui/Text";
|
|
||||||
import { Controls } from "./Controls";
|
import { Controls } from "./Controls";
|
||||||
|
import { Settings } from "./settings/Sheet";
|
||||||
|
|
||||||
const EpisodeSelector = ({
|
const EpisodeSelector = ({
|
||||||
seasonNumber,
|
seasonNumber,
|
||||||
setSelectedSeason,
|
setSelectedSeason,
|
||||||
closeModal,
|
...props
|
||||||
}: {
|
}: SheetProps & {
|
||||||
seasonNumber: number;
|
seasonNumber: number;
|
||||||
setSelectedSeason: (season: number | null) => void;
|
setSelectedSeason: (season: number | null) => void;
|
||||||
closeModal: () => void;
|
|
||||||
}) => {
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
const meta = usePlayerStore((state) => state.meta);
|
const meta = usePlayerStore((state) => state.meta);
|
||||||
const setMeta = usePlayerStore((state) => state.setMeta);
|
const setMeta = usePlayerStore((state) => state.setMeta);
|
||||||
|
|
||||||
@@ -42,38 +34,47 @@ const EpisodeSelector = ({
|
|||||||
if (!meta) return null;
|
if (!meta) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Settings.Sheet
|
||||||
{isLoading && (
|
open={props.open}
|
||||||
<View className="flex-1 items-center justify-center">
|
onOpenChange={props.onOpenChange}
|
||||||
<ActivityIndicator
|
{...props}
|
||||||
size="large"
|
|
||||||
color={defaultTheme.extend.colors.buttons.purple}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
{data && (
|
|
||||||
<ScrollView
|
|
||||||
className="flex-1 flex-col bg-gray-900"
|
|
||||||
contentContainerStyle={{
|
|
||||||
padding: 10,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<View className="flex-row items-center gap-4 p-2">
|
<Settings.SheetOverlay />
|
||||||
|
<Settings.SheetHandle />
|
||||||
|
<Settings.SheetFrame isLoading={isLoading}>
|
||||||
|
<Settings.Header
|
||||||
|
icon={
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="arrow-back"
|
name="arrow-back"
|
||||||
size={20}
|
size={24}
|
||||||
color="white"
|
color={theme.buttonSecondaryText.val}
|
||||||
onPress={() => setSelectedSeason(null)}
|
onPress={() => {
|
||||||
|
setSelectedSeason(null);
|
||||||
|
props.onOpenChange?.(false);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Text className="text-center font-bold">
|
}
|
||||||
Season {data.season_number}
|
title={`Season ${data?.season_number}`}
|
||||||
</Text>
|
/>
|
||||||
</View>
|
<Settings.Content>
|
||||||
<Divider />
|
{data?.episodes.map((episode) => (
|
||||||
{data.episodes.map((episode) => (
|
<Settings.Item
|
||||||
<TouchableOpacity
|
|
||||||
key={episode.id}
|
key={episode.id}
|
||||||
className="p-3"
|
iconLeft={
|
||||||
|
<View
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
backgroundColor="#121c24"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
borderRadius={6}
|
||||||
|
>
|
||||||
|
<Settings.Text fontSize={14}>
|
||||||
|
E{episode.episode_number}
|
||||||
|
</Settings.Text>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
title={episode.name}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setMeta({
|
setMeta({
|
||||||
...meta,
|
...meta,
|
||||||
@@ -82,26 +83,23 @@ const EpisodeSelector = ({
|
|||||||
tmdbId: episode.id.toString(),
|
tmdbId: episode.id.toString(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
closeModal();
|
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<Text>
|
|
||||||
E{episode.episode_number} {episode.name}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
))}
|
))}
|
||||||
</ScrollView>
|
</Settings.Content>
|
||||||
)}
|
</Settings.SheetFrame>
|
||||||
</>
|
</Settings.Sheet>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SeasonSelector = () => {
|
export const SeasonSelector = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [episodeOpen, setEpisodeOpen] = useState(false);
|
||||||
|
|
||||||
const [selectedSeason, setSelectedSeason] = useState<number | null>(null);
|
const [selectedSeason, setSelectedSeason] = useState<number | null>(null);
|
||||||
const meta = usePlayerStore((state) => state.meta);
|
const meta = usePlayerStore((state) => state.meta);
|
||||||
|
|
||||||
const { isTrue, on, off } = useBoolean();
|
|
||||||
|
|
||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading } = useQuery({
|
||||||
queryKey: ["seasons", meta!.tmdbId],
|
queryKey: ["seasons", meta!.tmdbId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
@@ -113,77 +111,74 @@ export const SeasonSelector = () => {
|
|||||||
if (meta?.type !== "show") return null;
|
if (meta?.type !== "show") return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="max-w-36 flex-1">
|
<>
|
||||||
<Controls>
|
<Controls>
|
||||||
<Button
|
<MWButton
|
||||||
title="Episode"
|
type="secondary"
|
||||||
variant="outline"
|
icon={
|
||||||
onPress={on}
|
|
||||||
iconLeft={
|
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name="audio-video"
|
name="audio-video"
|
||||||
size={24}
|
size={24}
|
||||||
color={defaultTheme.extend.colors.buttons.purple}
|
color={theme.buttonSecondaryText.val}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
onPress={() => setOpen(true)}
|
||||||
|
>
|
||||||
|
Episodes
|
||||||
|
</MWButton>
|
||||||
</Controls>
|
</Controls>
|
||||||
|
|
||||||
<Modal
|
<Settings.Sheet
|
||||||
isVisible={isTrue}
|
forceRemoveScrollEnabled={open}
|
||||||
onBackdropPress={off}
|
open={open}
|
||||||
supportedOrientations={["portrait", "landscape"]}
|
onOpenChange={setOpen}
|
||||||
style={{
|
|
||||||
width: "35%",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignSelf: "center",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{selectedSeason === null && (
|
<Settings.SheetOverlay />
|
||||||
<>
|
<Settings.SheetHandle />
|
||||||
{isLoading && (
|
<Settings.SheetFrame isLoading={isLoading}>
|
||||||
<View className="flex-1 items-center justify-center">
|
{episodeOpen && selectedSeason ? (
|
||||||
<ActivityIndicator
|
|
||||||
size="large"
|
|
||||||
color={defaultTheme.extend.colors.buttons.purple}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
{data && (
|
|
||||||
<ScrollView
|
|
||||||
className="flex-1 flex-col bg-gray-900"
|
|
||||||
contentContainerStyle={{
|
|
||||||
padding: 10,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text className="text-center font-bold">
|
|
||||||
{data.result.name}
|
|
||||||
</Text>
|
|
||||||
<Divider />
|
|
||||||
{data.result.seasons.map((season) => (
|
|
||||||
<TouchableOpacity
|
|
||||||
key={season.season_number}
|
|
||||||
className="m-1 flex flex-row items-center p-2"
|
|
||||||
onPress={() => setSelectedSeason(season.season_number)}
|
|
||||||
>
|
|
||||||
<Text className="flex-grow">
|
|
||||||
Season {season.season_number}
|
|
||||||
</Text>
|
|
||||||
<Ionicons name="chevron-forward" size={24} color="white" />
|
|
||||||
</TouchableOpacity>
|
|
||||||
))}
|
|
||||||
</ScrollView>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{selectedSeason !== null && (
|
|
||||||
<EpisodeSelector
|
<EpisodeSelector
|
||||||
seasonNumber={selectedSeason}
|
seasonNumber={selectedSeason}
|
||||||
setSelectedSeason={setSelectedSeason}
|
setSelectedSeason={setSelectedSeason}
|
||||||
closeModal={off}
|
open={episodeOpen}
|
||||||
|
onOpenChange={setEpisodeOpen}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Settings.Header
|
||||||
|
icon={
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name="close"
|
||||||
|
size={24}
|
||||||
|
color={theme.playerSettingsUnactiveText.val}
|
||||||
|
onPress={() => setOpen(false)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title={data?.result.name ?? ""}
|
||||||
|
/>
|
||||||
|
<Settings.Content>
|
||||||
|
{data?.result.seasons.map((season) => (
|
||||||
|
<Settings.Item
|
||||||
|
key={season.season_number}
|
||||||
|
title={`Season ${season.season_number}`}
|
||||||
|
iconRight={
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name="chevron-right"
|
||||||
|
size={24}
|
||||||
|
color="white"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onPress={() => {
|
||||||
|
setSelectedSeason(season.season_number);
|
||||||
|
setEpisodeOpen(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Settings.Content>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Settings.SheetFrame>
|
||||||
</View>
|
</Settings.Sheet>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -1,20 +1,18 @@
|
|||||||
import { useCallback, useState } from "react";
|
import type { SheetProps } from "tamagui";
|
||||||
import { ActivityIndicator, Pressable, ScrollView, View } from "react-native";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import Modal from "react-native-modal";
|
|
||||||
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
|
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
|
||||||
|
import { Spinner, Text, useTheme, View } from "tamagui";
|
||||||
|
|
||||||
import { getBuiltinSources, providers } from "@movie-web/provider-utils";
|
import { getBuiltinSources, providers } from "@movie-web/provider-utils";
|
||||||
import { defaultTheme } from "@movie-web/tailwind-config/themes";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useEmbedScrape,
|
useEmbedScrape,
|
||||||
useSourceScrape,
|
useSourceScrape,
|
||||||
} from "~/hooks/player/useSourceScrape";
|
} from "~/hooks/player/useSourceScrape";
|
||||||
import { useBoolean } from "~/hooks/useBoolean";
|
|
||||||
import { usePlayerStore } from "~/stores/player/store";
|
import { usePlayerStore } from "~/stores/player/store";
|
||||||
import { Button } from "../ui/Button";
|
import { MWButton } from "../ui/Button";
|
||||||
import { Text } from "../ui/Text";
|
|
||||||
import { Controls } from "./Controls";
|
import { Controls } from "./Controls";
|
||||||
|
import { Settings } from "./settings/Sheet";
|
||||||
|
|
||||||
const SourceItem = ({
|
const SourceItem = ({
|
||||||
name,
|
name,
|
||||||
@@ -22,20 +20,39 @@ const SourceItem = ({
|
|||||||
active,
|
active,
|
||||||
embed,
|
embed,
|
||||||
onPress,
|
onPress,
|
||||||
closeModal,
|
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
id: string;
|
id: string;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
embed?: { url: string; embedId: string };
|
embed?: { url: string; embedId: string };
|
||||||
onPress?: (id: string) => void;
|
onPress?: (id: string) => void;
|
||||||
closeModal?: () => void;
|
|
||||||
}) => {
|
}) => {
|
||||||
const { mutate, isPending, isError } = useEmbedScrape(closeModal);
|
const theme = useTheme();
|
||||||
|
const { mutate, isPending, isError } = useEmbedScrape();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Settings.Item
|
||||||
className="flex w-full flex-row justify-between p-3"
|
title={name}
|
||||||
|
iconRight={
|
||||||
|
<>
|
||||||
|
{active && (
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name="check-circle"
|
||||||
|
size={24}
|
||||||
|
color={theme.sheetItemSelected.val}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isError && (
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name="alert-circle"
|
||||||
|
size={24}
|
||||||
|
color={theme.scrapingError.val}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isPending && <Spinner size="small" color="$scrapingLoading" />}
|
||||||
|
</>
|
||||||
|
}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (onPress) {
|
if (onPress) {
|
||||||
onPress(id);
|
onPress(id);
|
||||||
@@ -49,53 +66,59 @@ const SourceItem = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
|
||||||
<Text className="font-bold">{name}</Text>
|
|
||||||
{active && (
|
|
||||||
<MaterialCommunityIcons
|
|
||||||
name="check-circle"
|
|
||||||
size={24}
|
|
||||||
color={defaultTheme.extend.colors.buttons.purple}
|
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
{isError && (
|
|
||||||
<MaterialCommunityIcons
|
|
||||||
name="alert-circle"
|
|
||||||
size={24}
|
|
||||||
color={defaultTheme.extend.colors.video.context.error}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isPending && <ActivityIndicator size="small" color="#0000ff" />}
|
|
||||||
</Pressable>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const EmbedsPart = ({
|
const EmbedsPart = ({
|
||||||
sourceId,
|
sourceId,
|
||||||
setCurrentScreen,
|
closeParent,
|
||||||
closeModal,
|
...props
|
||||||
}: {
|
}: SheetProps & {
|
||||||
sourceId: string;
|
sourceId: string;
|
||||||
setCurrentScreen: (screen: "source" | "embed") => void;
|
closeParent?: (open: boolean) => void;
|
||||||
closeModal: () => 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 (
|
return (
|
||||||
<View className="flex w-full flex-col gap-4 p-3">
|
<Settings.Sheet
|
||||||
<View className="flex-row items-center gap-4">
|
open={props.open}
|
||||||
|
onOpenChange={props.onOpenChange}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Settings.SheetOverlay />
|
||||||
|
<Settings.SheetHandle />
|
||||||
|
<Settings.SheetFrame>
|
||||||
|
<Settings.Header
|
||||||
|
icon={
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="arrow-back"
|
name="arrow-back"
|
||||||
size={30}
|
size={24}
|
||||||
color="white"
|
color={theme.buttonSecondaryText.val}
|
||||||
onPress={() => setCurrentScreen("source")}
|
onPress={() => {
|
||||||
|
props.onOpenChange?.(false);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Text className="text-xl font-bold">Embeds</Text>
|
}
|
||||||
|
title={providers.getMetadata(sourceId)?.name ?? "Embeds"}
|
||||||
|
/>
|
||||||
|
<Settings.Content>
|
||||||
|
<View alignItems="center" justifyContent="center">
|
||||||
|
{isPending && <Spinner size="small" color="$loadingIndicator" />}
|
||||||
|
{error && <Text>Something went wrong!</Text>}
|
||||||
</View>
|
</View>
|
||||||
{isPending && <ActivityIndicator size="small" color="#0000ff" />}
|
|
||||||
{error && <Text>{error.message}</Text>}
|
|
||||||
{data && data?.length > 1 && (
|
{data && data?.length > 1 && (
|
||||||
<View className="flex w-full flex-col p-3">
|
<Settings.Content>
|
||||||
{data.map((embed) => {
|
{data.map((embed) => {
|
||||||
const metaData = providers.getMetadata(embed.embedId)!;
|
const metaData = providers.getMetadata(embed.embedId)!;
|
||||||
return (
|
return (
|
||||||
@@ -104,25 +127,25 @@ const EmbedsPart = ({
|
|||||||
name={metaData.name}
|
name={metaData.name}
|
||||||
id={embed.embedId}
|
id={embed.embedId}
|
||||||
embed={embed}
|
embed={embed}
|
||||||
closeModal={closeModal}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</View>
|
</Settings.Content>
|
||||||
)}
|
)}
|
||||||
</View>
|
</Settings.Content>
|
||||||
|
</Settings.SheetFrame>
|
||||||
|
</Settings.Sheet>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SourceSelector = () => {
|
export const SourceSelector = () => {
|
||||||
const [currentScreen, setCurrentScreen] = useState<"source" | "embed">(
|
const theme = useTheme();
|
||||||
"source",
|
const [open, setOpen] = useState(false);
|
||||||
);
|
const [embedOpen, setEmbedOpen] = useState(false);
|
||||||
|
|
||||||
const sourceId = usePlayerStore((state) => state.interface.sourceId);
|
const sourceId = usePlayerStore((state) => state.interface.sourceId);
|
||||||
const setSourceId = usePlayerStore((state) => state.setSourceId);
|
const setSourceId = usePlayerStore((state) => state.setSourceId);
|
||||||
|
|
||||||
const { isTrue, on, off } = useBoolean();
|
|
||||||
|
|
||||||
const isActive = useCallback(
|
const isActive = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
return sourceId === id;
|
return sourceId === id;
|
||||||
@@ -131,40 +154,52 @@ export const SourceSelector = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="max-w-36">
|
<>
|
||||||
<Controls>
|
<Controls>
|
||||||
<Button
|
<MWButton
|
||||||
title="Source"
|
type="secondary"
|
||||||
variant="outline"
|
icon={
|
||||||
onPress={on}
|
|
||||||
iconLeft={
|
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name="video"
|
name="video"
|
||||||
size={24}
|
size={24}
|
||||||
color={defaultTheme.extend.colors.buttons.purple}
|
color={theme.buttonSecondaryText.val}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
onPress={() => setOpen(true)}
|
||||||
|
>
|
||||||
|
Source
|
||||||
|
</MWButton>
|
||||||
</Controls>
|
</Controls>
|
||||||
|
|
||||||
<Modal
|
<Settings.Sheet
|
||||||
isVisible={isTrue}
|
forceRemoveScrollEnabled={open}
|
||||||
onBackdropPress={off}
|
open={open}
|
||||||
supportedOrientations={["portrait", "landscape"]}
|
onOpenChange={setOpen}
|
||||||
style={{
|
|
||||||
width: "35%",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignSelf: "center",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<ScrollView
|
<Settings.SheetOverlay />
|
||||||
className="w-full flex-1 bg-gray-900"
|
<Settings.SheetHandle />
|
||||||
contentContainerStyle={{
|
<Settings.SheetFrame>
|
||||||
padding: 10,
|
{embedOpen && sourceId ? (
|
||||||
}}
|
<EmbedsPart
|
||||||
>
|
sourceId={sourceId}
|
||||||
{currentScreen === "source" && (
|
open={embedOpen}
|
||||||
|
onOpenChange={setEmbedOpen}
|
||||||
|
closeParent={setOpen}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<>
|
<>
|
||||||
|
<Settings.Header
|
||||||
|
icon={
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name="close"
|
||||||
|
size={24}
|
||||||
|
color={theme.playerSettingsUnactiveText.val}
|
||||||
|
onPress={() => setOpen(false)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title="Sources"
|
||||||
|
/>
|
||||||
|
<Settings.Content>
|
||||||
{getBuiltinSources()
|
{getBuiltinSources()
|
||||||
.sort((a, b) => b.rank - a.rank)
|
.sort((a, b) => b.rank - a.rank)
|
||||||
.map((source) => (
|
.map((source) => (
|
||||||
@@ -173,23 +208,17 @@ export const SourceSelector = () => {
|
|||||||
name={source.name}
|
name={source.name}
|
||||||
id={source.id}
|
id={source.id}
|
||||||
active={isActive(source.id)}
|
active={isActive(source.id)}
|
||||||
onPress={() => {
|
onPress={(id) => {
|
||||||
setSourceId(source.id);
|
setSourceId(id);
|
||||||
setCurrentScreen("embed");
|
setEmbedOpen(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
</Settings.Content>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{currentScreen === "embed" && (
|
</Settings.SheetFrame>
|
||||||
<EmbedsPart
|
</Settings.Sheet>
|
||||||
sourceId={sourceId!}
|
</>
|
||||||
setCurrentScreen={setCurrentScreen}
|
|
||||||
closeModal={off}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</ScrollView>
|
|
||||||
</Modal>
|
|
||||||
</View>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { StyleSheet, View } from "react-native";
|
|
||||||
import Animated, {
|
import Animated, {
|
||||||
Easing,
|
Easing,
|
||||||
useAnimatedProps,
|
useAnimatedProps,
|
||||||
@@ -8,6 +7,7 @@ import Animated, {
|
|||||||
} from "react-native-reanimated";
|
} from "react-native-reanimated";
|
||||||
import { Circle, Svg } from "react-native-svg";
|
import { Circle, Svg } from "react-native-svg";
|
||||||
import { AntDesign } from "@expo/vector-icons";
|
import { AntDesign } from "@expo/vector-icons";
|
||||||
|
import { View } from "tamagui";
|
||||||
|
|
||||||
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
|
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ export const StatusCircle = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View justifyContent="center" alignItems="center" position="relative">
|
||||||
<Svg height="60" width="60" viewBox="0 0 60 60">
|
<Svg height="60" width="60" viewBox="0 0 60 60">
|
||||||
{type === "loading" && (
|
{type === "loading" && (
|
||||||
<AnimatedCircle
|
<AnimatedCircle
|
||||||
@@ -70,11 +70,3 @@ export const StatusCircle = ({
|
|||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
position: "relative",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
@@ -1,12 +1,6 @@
|
|||||||
import type { AVPlaybackSource } from "expo-av";
|
import type { AVPlaybackSource } from "expo-av";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import { Dimensions, Platform } from "react-native";
|
||||||
ActivityIndicator,
|
|
||||||
Dimensions,
|
|
||||||
Platform,
|
|
||||||
StyleSheet,
|
|
||||||
View,
|
|
||||||
} from "react-native";
|
|
||||||
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
||||||
import { runOnJS, useSharedValue } from "react-native-reanimated";
|
import { runOnJS, useSharedValue } from "react-native-reanimated";
|
||||||
import { ResizeMode, Video } from "expo-av";
|
import { ResizeMode, Video } from "expo-av";
|
||||||
@@ -14,6 +8,7 @@ import * as Haptics from "expo-haptics";
|
|||||||
import * as NavigationBar from "expo-navigation-bar";
|
import * as NavigationBar from "expo-navigation-bar";
|
||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
import * as StatusBar from "expo-status-bar";
|
import * as StatusBar from "expo-status-bar";
|
||||||
|
import { Spinner, Text, View } from "tamagui";
|
||||||
|
|
||||||
import { findHighestQuality } from "@movie-web/provider-utils";
|
import { findHighestQuality } from "@movie-web/provider-utils";
|
||||||
|
|
||||||
@@ -24,7 +19,6 @@ import { usePlayer } from "~/hooks/player/usePlayer";
|
|||||||
import { useVolume } from "~/hooks/player/useVolume";
|
import { useVolume } from "~/hooks/player/useVolume";
|
||||||
import { useAudioTrackStore } from "~/stores/audio";
|
import { useAudioTrackStore } from "~/stores/audio";
|
||||||
import { usePlayerStore } from "~/stores/player/store";
|
import { usePlayerStore } from "~/stores/player/store";
|
||||||
import { Text } from "../ui/Text";
|
|
||||||
import { CaptionRenderer } from "./CaptionRenderer";
|
import { CaptionRenderer } from "./CaptionRenderer";
|
||||||
import { ControlsOverlay } from "./ControlsOverlay";
|
import { ControlsOverlay } from "./ControlsOverlay";
|
||||||
import { isPointInSliderVicinity } from "./VideoSlider";
|
import { isPointInSliderVicinity } from "./VideoSlider";
|
||||||
@@ -231,7 +225,13 @@ export const VideoPlayer = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<GestureDetector gesture={composedGesture}>
|
<GestureDetector gesture={composedGesture}>
|
||||||
<View className="flex-1 items-center justify-center bg-black">
|
<View
|
||||||
|
flex={1}
|
||||||
|
flexDirection="row"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
backgroundColor="black"
|
||||||
|
>
|
||||||
<Video
|
<Video
|
||||||
ref={setVideoRef}
|
ref={setVideoRef}
|
||||||
source={videoSrc}
|
source={videoSrc}
|
||||||
@@ -243,8 +243,12 @@ export const VideoPlayer = () => {
|
|||||||
onReadyForDisplay={onReadyForDisplay}
|
onReadyForDisplay={onReadyForDisplay}
|
||||||
onPlaybackStatusUpdate={setStatus}
|
onPlaybackStatusUpdate={setStatus}
|
||||||
style={[
|
style={[
|
||||||
styles.video,
|
|
||||||
{
|
{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
...(!isIdle && {
|
...(!isIdle && {
|
||||||
opacity: 0.7,
|
opacity: 0.7,
|
||||||
}),
|
}),
|
||||||
@@ -252,24 +256,47 @@ export const VideoPlayer = () => {
|
|||||||
]}
|
]}
|
||||||
onTouchStart={() => setIsIdle(!isIdle)}
|
onTouchStart={() => setIsIdle(!isIdle)}
|
||||||
/>
|
/>
|
||||||
<View className="h-full w-full flex-1 items-center justify-center">
|
<View
|
||||||
|
height="100%"
|
||||||
|
width="100%"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
>
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<ActivityIndicator
|
<Spinner
|
||||||
size="large"
|
size="large"
|
||||||
color="#0000ff"
|
color="$loadingIndicator"
|
||||||
className="absolute"
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ControlsOverlay isLoading={isLoading} />
|
<ControlsOverlay isLoading={isLoading} />
|
||||||
</View>
|
</View>
|
||||||
{showVolumeOverlay && (
|
{showVolumeOverlay && (
|
||||||
<View className="absolute bottom-12 self-center rounded-xl bg-black p-3 opacity-50">
|
<View
|
||||||
<Text className="font-bold">Volume: {debouncedVolume}</Text>
|
position="absolute"
|
||||||
|
bottom={48}
|
||||||
|
alignSelf="center"
|
||||||
|
borderRadius={999}
|
||||||
|
backgroundColor="black"
|
||||||
|
padding={12}
|
||||||
|
opacity={0.5}
|
||||||
|
>
|
||||||
|
<Text fontWeight="bold">Volume: {debouncedVolume}</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{showBrightnessOverlay && (
|
{showBrightnessOverlay && (
|
||||||
<View className="absolute bottom-12 self-center rounded-xl bg-black p-3 opacity-50">
|
<View
|
||||||
<Text className="font-bold">Brightness: {debouncedBrightness}</Text>
|
position="absolute"
|
||||||
|
bottom={48}
|
||||||
|
alignSelf="center"
|
||||||
|
borderRadius={999}
|
||||||
|
backgroundColor="black"
|
||||||
|
padding={12}
|
||||||
|
opacity={0.5}
|
||||||
|
>
|
||||||
|
<Text fontWeight="bold">Brightness: {debouncedBrightness}</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
<CaptionRenderer />
|
<CaptionRenderer />
|
||||||
@@ -277,13 +304,3 @@ export const VideoPlayer = () => {
|
|||||||
</GestureDetector>
|
</GestureDetector>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
video: {
|
|
||||||
position: "absolute",
|
|
||||||
top: 0,
|
|
||||||
bottom: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
@@ -4,7 +4,7 @@ import type {
|
|||||||
TapGestureHandlerEventPayload,
|
TapGestureHandlerEventPayload,
|
||||||
} from "react-native-gesture-handler";
|
} from "react-native-gesture-handler";
|
||||||
import React, { useEffect, useRef } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import { Dimensions, StyleSheet, View } from "react-native";
|
import { Dimensions } from "react-native";
|
||||||
import {
|
import {
|
||||||
PanGestureHandler,
|
PanGestureHandler,
|
||||||
State,
|
State,
|
||||||
@@ -16,8 +16,7 @@ import Animated, {
|
|||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
} from "react-native-reanimated";
|
} from "react-native-reanimated";
|
||||||
|
import { useTheme, View } from "tamagui";
|
||||||
import { defaultTheme } from "@movie-web/tailwind-config/themes";
|
|
||||||
|
|
||||||
import { usePlayerStore } from "~/stores/player/store";
|
import { usePlayerStore } from "~/stores/player/store";
|
||||||
|
|
||||||
@@ -47,6 +46,7 @@ export const isPointInSliderVicinity = (x: number, y: number) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const VideoSlider = ({ onSlidingComplete }: VideoSliderProps) => {
|
const VideoSlider = ({ onSlidingComplete }: VideoSliderProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
const tapRef = useRef<TapGestureHandler>(null);
|
const tapRef = useRef<TapGestureHandler>(null);
|
||||||
const panRef = useRef<PanGestureHandler>(null);
|
const panRef = useRef<PanGestureHandler>(null);
|
||||||
const status = usePlayerStore((state) => state.status);
|
const status = usePlayerStore((state) => state.status);
|
||||||
@@ -143,13 +143,13 @@ const VideoSlider = ({ onSlidingComplete }: VideoSliderProps) => {
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
className="justify-center"
|
|
||||||
style={[
|
style={[
|
||||||
{
|
{
|
||||||
height: trackSize_,
|
height: trackSize_,
|
||||||
borderRadius: trackSize_,
|
borderRadius: trackSize_,
|
||||||
backgroundColor: defaultTheme.extend.colors.video.context.slider,
|
backgroundColor: theme.videoSlider.val,
|
||||||
width,
|
width,
|
||||||
|
justifyContent: "center",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -158,8 +158,7 @@ const VideoSlider = ({ onSlidingComplete }: VideoSliderProps) => {
|
|||||||
{
|
{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
height: trackSize_,
|
height: trackSize_,
|
||||||
backgroundColor:
|
backgroundColor: theme.videoSliderFilled.val,
|
||||||
defaultTheme.extend.colors.video.context.sliderFilled,
|
|
||||||
borderRadius: trackSize_ / 2,
|
borderRadius: trackSize_ / 2,
|
||||||
},
|
},
|
||||||
progressStyle,
|
progressStyle,
|
||||||
@@ -172,13 +171,13 @@ const VideoSlider = ({ onSlidingComplete }: VideoSliderProps) => {
|
|||||||
>
|
>
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[
|
style={[
|
||||||
styles.knob,
|
|
||||||
{
|
{
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
height: knobSize_,
|
height: knobSize_,
|
||||||
width: knobSize_,
|
width: knobSize_,
|
||||||
borderRadius: knobSize_ / 2,
|
borderRadius: knobSize_ / 2,
|
||||||
backgroundColor:
|
backgroundColor: theme.videoSliderFilled.val,
|
||||||
defaultTheme.extend.colors.video.context.sliderFilled,
|
|
||||||
},
|
},
|
||||||
scrollTranslationStyle,
|
scrollTranslationStyle,
|
||||||
]}
|
]}
|
||||||
@@ -190,11 +189,4 @@ const VideoSlider = ({ onSlidingComplete }: VideoSliderProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
knob: {
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default VideoSlider;
|
export default VideoSlider;
|
||||||
|
144
apps/expo/src/components/player/settings/Sheet.tsx
Normal file
144
apps/expo/src/components/player/settings/Sheet.tsx
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import type { SheetProps, ViewProps } from "tamagui";
|
||||||
|
import {
|
||||||
|
ScrollView,
|
||||||
|
Separator,
|
||||||
|
Sheet,
|
||||||
|
Spinner,
|
||||||
|
styled,
|
||||||
|
Text,
|
||||||
|
View,
|
||||||
|
} from "tamagui";
|
||||||
|
|
||||||
|
const PlayerText = styled(Text, {
|
||||||
|
color: "$playerSettingsUnactiveText",
|
||||||
|
fontWeight: "bold",
|
||||||
|
fontSize: 18,
|
||||||
|
});
|
||||||
|
|
||||||
|
function SettingsSheet(props: SheetProps) {
|
||||||
|
return (
|
||||||
|
<Sheet
|
||||||
|
snapPoints={[90]}
|
||||||
|
dismissOnSnapToBottom
|
||||||
|
modal
|
||||||
|
animation="spring"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SettingsSheetOverlay() {
|
||||||
|
return (
|
||||||
|
<Sheet.Overlay
|
||||||
|
animation="lazy"
|
||||||
|
backgroundColor="rgba(0, 0, 0, 0.7)"
|
||||||
|
enterStyle={{ opacity: 0 }}
|
||||||
|
exitStyle={{ opacity: 0 }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SettingsSheetHandle() {
|
||||||
|
return <Sheet.Handle backgroundColor="$sheetHandle" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SettingsSheetFrame({
|
||||||
|
children,
|
||||||
|
isLoading,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
isLoading?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Sheet.Frame
|
||||||
|
backgroundColor="$playerSettingsBackground"
|
||||||
|
padding="$5"
|
||||||
|
gap="$4"
|
||||||
|
>
|
||||||
|
{isLoading && (
|
||||||
|
<Spinner
|
||||||
|
size="large"
|
||||||
|
color="$loadingIndicator"
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!isLoading && children}
|
||||||
|
</Sheet.Frame>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SettingsHeader({
|
||||||
|
icon,
|
||||||
|
title,
|
||||||
|
rightButton,
|
||||||
|
}: {
|
||||||
|
icon: React.ReactNode;
|
||||||
|
title: string;
|
||||||
|
rightButton?: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<View flexDirection="row" alignItems="center" gap="$4">
|
||||||
|
{icon}
|
||||||
|
<PlayerText flexGrow={1}>{title}</PlayerText>
|
||||||
|
{rightButton}
|
||||||
|
</View>
|
||||||
|
<Separator />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SettingsContent({
|
||||||
|
isScroll = true,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
isScroll?: boolean;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const ViewDisplay = isScroll ? ScrollView : View;
|
||||||
|
return (
|
||||||
|
<ViewDisplay
|
||||||
|
contentContainerStyle={{
|
||||||
|
gap: "$4",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ViewDisplay>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SettingsItem({
|
||||||
|
iconLeft,
|
||||||
|
iconRight,
|
||||||
|
title,
|
||||||
|
...props
|
||||||
|
}: ViewProps & {
|
||||||
|
iconLeft?: React.ReactNode;
|
||||||
|
iconRight?: React.ReactNode;
|
||||||
|
title: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<View flexDirection="row" gap="$4" alignItems="center" {...props}>
|
||||||
|
{iconLeft}
|
||||||
|
<PlayerText flexGrow={1} fontSize={16} fontWeight="700">
|
||||||
|
{title}
|
||||||
|
</PlayerText>
|
||||||
|
{iconRight}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Settings = {
|
||||||
|
Sheet: SettingsSheet,
|
||||||
|
SheetOverlay: SettingsSheetOverlay,
|
||||||
|
SheetHandle: SettingsSheetHandle,
|
||||||
|
SheetFrame: SettingsSheetFrame,
|
||||||
|
Header: SettingsHeader,
|
||||||
|
Content: SettingsContent,
|
||||||
|
Text: PlayerText,
|
||||||
|
Item: SettingsItem,
|
||||||
|
};
|
@@ -1,60 +1,51 @@
|
|||||||
import type { VariantProps } from "class-variance-authority";
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
import type { ReactNode } from "react";
|
import type { ButtonProps } from "tamagui";
|
||||||
import type { PressableProps } from "react-native";
|
import React from "react";
|
||||||
import { Pressable } from "react-native";
|
import { Button, styled } from "tamagui";
|
||||||
import { cva } from "class-variance-authority";
|
|
||||||
|
|
||||||
import { cn } from "~/lib/utils";
|
const PrimaryButton = styled(Button, {
|
||||||
import { Text } from "./Text";
|
backgroundColor: "$buttonPrimaryBackground",
|
||||||
|
color: "$buttonPrimaryText",
|
||||||
|
fontWeight: "bold",
|
||||||
|
});
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const SecondaryButton = styled(Button, {
|
||||||
"flex flex-row items-center justify-center gap-4 rounded-md disabled:opacity-50",
|
backgroundColor: "$buttonSecondaryBackground",
|
||||||
{
|
color: "$buttonSecondaryText",
|
||||||
variants: {
|
fontWeight: "bold",
|
||||||
variant: {
|
});
|
||||||
default: "bg-buttons-purple",
|
|
||||||
outline: "border border-buttons-purple bg-transparent",
|
|
||||||
secondary: "bg-buttons-secondary",
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
default: "h-10 px-4 py-2",
|
|
||||||
sm: "h-9 rounded-md px-3",
|
|
||||||
lg: "h-11 rounded-md px-8",
|
|
||||||
icon: "h-10 w-10",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
size: "default",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export interface ButtonProps
|
const PurpleButton = styled(Button, {
|
||||||
extends PressableProps,
|
backgroundColor: "$buttonPurpleBackground",
|
||||||
VariantProps<typeof buttonVariants> {
|
color: "white",
|
||||||
iconLeft?: ReactNode;
|
fontWeight: "bold",
|
||||||
iconRight?: ReactNode;
|
});
|
||||||
title: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Button({
|
const CancelButton = styled(Button, {
|
||||||
onPress,
|
backgroundColor: "$buttonCancelBackground",
|
||||||
variant,
|
color: "white",
|
||||||
size,
|
fontWeight: "bold",
|
||||||
className,
|
});
|
||||||
iconLeft,
|
|
||||||
iconRight,
|
export const MWButton = React.forwardRef<
|
||||||
title,
|
typeof Button,
|
||||||
}: ButtonProps) {
|
ButtonProps & {
|
||||||
return (
|
type?: "primary" | "secondary" | "purple" | "cancel";
|
||||||
<Pressable
|
}
|
||||||
onPress={onPress}
|
>((props, ref) => {
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
const { type, ...rest } = props;
|
||||||
>
|
switch (type) {
|
||||||
{iconLeft}
|
case "primary":
|
||||||
<Text className="font-bold">{title}</Text>
|
return <PrimaryButton {...rest} ref={ref as any} />;
|
||||||
{iconRight}
|
case "secondary":
|
||||||
</Pressable>
|
return <SecondaryButton {...rest} ref={ref as any} />;
|
||||||
);
|
case "purple":
|
||||||
}
|
return <PurpleButton {...rest} ref={ref as any} />;
|
||||||
|
case "cancel":
|
||||||
|
return <CancelButton {...rest} ref={ref as any} />;
|
||||||
|
default:
|
||||||
|
return <Button {...rest} ref={ref as any} />;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
MWButton.displayName = "MWButton";
|
||||||
|
@@ -1,5 +0,0 @@
|
|||||||
import { View } from "react-native";
|
|
||||||
|
|
||||||
export const Divider = () => {
|
|
||||||
return <View className="mx-5 my-3 h-px border-t-0 bg-slate-600" />;
|
|
||||||
};
|
|
@@ -1,18 +1,30 @@
|
|||||||
import { useContext, useEffect, useRef, useState } from "react";
|
import { useContext, useEffect, useRef, useState } from "react";
|
||||||
import { TextInput, View } from "react-native";
|
import { Keyboard } from "react-native";
|
||||||
import { FontAwesome5 } from "@expo/vector-icons";
|
import { FontAwesome5 } from "@expo/vector-icons";
|
||||||
|
import { Input, styled, useTheme, View } from "tamagui";
|
||||||
import { defaultTheme } from "@movie-web/tailwind-config/themes";
|
|
||||||
|
|
||||||
import SearchTabContext from "./SearchTabContext";
|
import SearchTabContext from "./SearchTabContext";
|
||||||
|
|
||||||
|
const SearchInput = styled(Input, {
|
||||||
|
backgroundColor: "$searchBackground",
|
||||||
|
borderColor: "$colorTransparent",
|
||||||
|
placeholderTextColor: "$searchPlaceholder",
|
||||||
|
outlineStyle: "none",
|
||||||
|
focusStyle: {
|
||||||
|
borderColor: "$colorTransparent",
|
||||||
|
backgroundColor: "$searchFocused",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export function SearchBar({
|
export function SearchBar({
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
}: {
|
}: {
|
||||||
onSearchChange: (text: string) => void;
|
onSearchChange: (text: string) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const theme = useTheme();
|
||||||
const [keyword, setKeyword] = useState("");
|
const [keyword, setKeyword] = useState("");
|
||||||
const inputRef = useRef<TextInput>(null);
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
const inputRef = useRef<Input>(null);
|
||||||
|
|
||||||
const { focusSearchInputRef } = useContext(SearchTabContext);
|
const { focusSearchInputRef } = useContext(SearchTabContext);
|
||||||
|
|
||||||
@@ -22,27 +34,49 @@ export function SearchBar({
|
|||||||
};
|
};
|
||||||
}, [focusSearchInputRef]);
|
}, [focusSearchInputRef]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const keyboardDidShowListener = Keyboard.addListener(
|
||||||
|
"keyboardDidShow",
|
||||||
|
() => {
|
||||||
|
setIsFocused(true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const keyboardDidHideListener = Keyboard.addListener(
|
||||||
|
"keyboardDidHide",
|
||||||
|
() => {
|
||||||
|
setIsFocused(false);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
keyboardDidShowListener.remove();
|
||||||
|
keyboardDidHideListener.remove();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleChange = (text: string) => {
|
const handleChange = (text: string) => {
|
||||||
setKeyword(text);
|
setKeyword(text);
|
||||||
onSearchChange(text);
|
onSearchChange(text);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="border-primary-400 focus-within:border-primary-300 mb-6 mt-4 flex-row items-center rounded-full border bg-black">
|
<View
|
||||||
<View className="ml-1 w-12 items-center justify-center">
|
marginBottom={12}
|
||||||
<FontAwesome5
|
flexDirection="row"
|
||||||
name="search"
|
alignItems="center"
|
||||||
size={18}
|
borderRadius={999}
|
||||||
color={defaultTheme.extend.colors.search.icon}
|
borderWidth={1}
|
||||||
/>
|
backgroundColor={isFocused ? theme.searchFocused : theme.searchBackground}
|
||||||
|
>
|
||||||
|
<View width={48} alignItems="center" justifyContent="center">
|
||||||
|
<FontAwesome5 name="search" size={18} color={theme.searchIcon.val} />
|
||||||
</View>
|
</View>
|
||||||
<TextInput
|
<SearchInput
|
||||||
value={keyword}
|
value={keyword}
|
||||||
onChangeText={handleChange}
|
onChangeText={handleChange}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
placeholder="What are you looking for?"
|
placeholder="What are you looking for?"
|
||||||
placeholderTextColor={defaultTheme.extend.colors.search.placeholder}
|
width="80%"
|
||||||
className="w-full rounded-3xl py-3 pr-5 text-white"
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
@@ -1,18 +0,0 @@
|
|||||||
import type { TextProps } from "react-native";
|
|
||||||
import { Text as RNText } from "react-native";
|
|
||||||
import { cva } from "class-variance-authority";
|
|
||||||
|
|
||||||
import { cn } from "~/lib/utils";
|
|
||||||
|
|
||||||
const textVariants = cva("text-white");
|
|
||||||
|
|
||||||
export function Text({ className, ...props }: TextProps) {
|
|
||||||
return (
|
|
||||||
<RNText
|
|
||||||
className={cn(className, textVariants(), {
|
|
||||||
"font-sans": !className?.includes("font-"),
|
|
||||||
})}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -6,9 +6,9 @@ export const usePlaybackSpeed = () => {
|
|||||||
const videoRef = usePlayerStore((state) => state.videoRef);
|
const videoRef = usePlayerStore((state) => state.videoRef);
|
||||||
|
|
||||||
const changePlaybackSpeed = useCallback(
|
const changePlaybackSpeed = useCallback(
|
||||||
(newValue: number) => {
|
async (newValue: number) => {
|
||||||
if (videoRef) {
|
if (videoRef) {
|
||||||
void videoRef.setRateAsync(newValue, true);
|
await videoRef.setRateAsync(newValue, true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[videoRef],
|
[videoRef],
|
||||||
|
@@ -189,7 +189,7 @@ export function useScrape() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useEmbedScrape = (closeModal?: () => void) => {
|
export const useEmbedScrape = () => {
|
||||||
const setCurrentStream = usePlayerStore((state) => state.setCurrentStream);
|
const setCurrentStream = usePlayerStore((state) => state.setCurrentStream);
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -208,8 +208,8 @@ export const useEmbedScrape = (closeModal?: () => void) => {
|
|||||||
url,
|
url,
|
||||||
embedId,
|
embedId,
|
||||||
});
|
});
|
||||||
|
if (!result) throw new Error("no result");
|
||||||
if (result?.stream) {
|
if (result?.stream) {
|
||||||
closeModal?.();
|
|
||||||
setCurrentStream(result.stream[0]!);
|
setCurrentStream(result.stream[0]!);
|
||||||
return result.stream;
|
return result.stream;
|
||||||
}
|
}
|
||||||
@@ -224,10 +224,7 @@ export const useEmbedScrape = (closeModal?: () => void) => {
|
|||||||
return mutate;
|
return mutate;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useSourceScrape = (
|
export const useSourceScrape = (sourceId: string | null) => {
|
||||||
sourceId: string | null,
|
|
||||||
closeModal: () => void,
|
|
||||||
) => {
|
|
||||||
const meta = usePlayerStore((state) => state.meta);
|
const meta = usePlayerStore((state) => state.meta);
|
||||||
const setCurrentStream = usePlayerStore((state) => state.setCurrentStream);
|
const setCurrentStream = usePlayerStore((state) => state.setCurrentStream);
|
||||||
const setSourceId = usePlayerStore((state) => state.setSourceId);
|
const setSourceId = usePlayerStore((state) => state.setSourceId);
|
||||||
@@ -235,6 +232,7 @@ export const useSourceScrape = (
|
|||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
queryKey: ["sourceScrape", meta, sourceId],
|
queryKey: ["sourceScrape", meta, sourceId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
|
console.log("useSourceScrape", meta, sourceId);
|
||||||
if (!meta || !sourceId) return;
|
if (!meta || !sourceId) return;
|
||||||
const scrapeMedia = convertMetaToScrapeMedia(meta);
|
const scrapeMedia = convertMetaToScrapeMedia(meta);
|
||||||
const result = await getVideoStreamFromSource({
|
const result = await getVideoStreamFromSource({
|
||||||
@@ -242,13 +240,13 @@ export const useSourceScrape = (
|
|||||||
media: scrapeMedia,
|
media: scrapeMedia,
|
||||||
events: {
|
events: {
|
||||||
update(evt) {
|
update(evt) {
|
||||||
console.log(evt);
|
console.log("update useSourceScrape", evt);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
console.log("useSourceScrape result", result);
|
||||||
|
|
||||||
if (result?.stream) {
|
if (result?.stream) {
|
||||||
closeModal();
|
|
||||||
setCurrentStream(result.stream[0]!);
|
setCurrentStream(result.stream[0]!);
|
||||||
setSourceId(sourceId);
|
setSourceId(sourceId);
|
||||||
return [];
|
return [];
|
||||||
@@ -256,7 +254,6 @@ export const useSourceScrape = (
|
|||||||
if (result?.embeds.length === 1) {
|
if (result?.embeds.length === 1) {
|
||||||
const embedResult = await getVideoStreamFromEmbed(result.embeds[0]!);
|
const embedResult = await getVideoStreamFromEmbed(result.embeds[0]!);
|
||||||
if (embedResult?.stream) {
|
if (embedResult?.stream) {
|
||||||
closeModal();
|
|
||||||
setCurrentStream(embedResult.stream[0]!);
|
setCurrentStream(embedResult.stream[0]!);
|
||||||
setSourceId(sourceId);
|
setSourceId(sourceId);
|
||||||
return [];
|
return [];
|
||||||
|
@@ -1,7 +0,0 @@
|
|||||||
import type { ClassValue } from "clsx";
|
|
||||||
import { clsx } from "clsx";
|
|
||||||
import { twMerge } from "tailwind-merge";
|
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
|
||||||
return twMerge(clsx(inputs));
|
|
||||||
}
|
|
@@ -4,7 +4,7 @@ import { immer } from "zustand/middleware/immer";
|
|||||||
|
|
||||||
import type { Stream } from "@movie-web/provider-utils";
|
import type { Stream } from "@movie-web/provider-utils";
|
||||||
|
|
||||||
type CaptionWithData = Stream["captions"][0] & {
|
export type CaptionWithData = Stream["captions"][0] & {
|
||||||
data: ContentCaption[];
|
data: ContentCaption[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
20
apps/expo/src/stores/theme/index.ts
Normal file
20
apps/expo/src/stores/theme/index.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import { immer } from "zustand/middleware/immer";
|
||||||
|
|
||||||
|
export type ThemeStoreOption = "main" | "blue" | "gray" | "red" | "teal";
|
||||||
|
|
||||||
|
export interface ThemeStore {
|
||||||
|
theme: ThemeStoreOption;
|
||||||
|
setTheme(v: ThemeStoreOption): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useThemeStore = create(
|
||||||
|
immer<ThemeStore>((set) => ({
|
||||||
|
theme: "main",
|
||||||
|
setTheme(v) {
|
||||||
|
set((s) => {
|
||||||
|
s.theme = v;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
);
|
@@ -1,3 +0,0 @@
|
|||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
21
apps/expo/src/types/country-language.d.ts
vendored
Normal file
21
apps/expo/src/types/country-language.d.ts
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
declare module "@ladjs/country-language" {
|
||||||
|
export interface LanguageObj {
|
||||||
|
countries: {
|
||||||
|
code_2: string;
|
||||||
|
code_3: string;
|
||||||
|
numCode: string;
|
||||||
|
}[];
|
||||||
|
direction: "RTL" | "LTR";
|
||||||
|
name: string[];
|
||||||
|
nativeName: string[];
|
||||||
|
iso639_1: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Callback<T> = (err: null | string, result: null | T) => void;
|
||||||
|
|
||||||
|
declare namespace lib {
|
||||||
|
function getLanguage(locale: string, cb: Callback<LanguageObj>): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export = lib;
|
||||||
|
}
|
1
apps/expo/src/types/nativewind-env.d.ts
vendored
1
apps/expo/src/types/nativewind-env.d.ts
vendored
@@ -1 +0,0 @@
|
|||||||
/// <reference types="nativewind/types" />
|
|
@@ -1,28 +0,0 @@
|
|||||||
import type { Config } from "tailwindcss";
|
|
||||||
// @ts-expect-error - no types
|
|
||||||
import nativewind from "nativewind/preset";
|
|
||||||
|
|
||||||
import baseConfig from "@movie-web/tailwind-config/native";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
content: ["./src/**/*.{ts,tsx}"],
|
|
||||||
presets: [
|
|
||||||
nativewind,
|
|
||||||
baseConfig,
|
|
||||||
{
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
fontFamily: {
|
|
||||||
sans: ["OpenSansRegular"],
|
|
||||||
thin: ["OpenSansLight"],
|
|
||||||
normal: ["OpenSansRegular"],
|
|
||||||
medium: ["OpenSansMedium"],
|
|
||||||
semibold: ["OpenSansSemiBold"],
|
|
||||||
bold: ["OpenSansBold"],
|
|
||||||
extrabold: ["OpenSansExtra"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
} satisfies Config;
|
|
876
apps/expo/tamagui-web.css
Normal file
876
apps/expo/tamagui-web.css
Normal file
File diff suppressed because one or more lines are too long
139
apps/expo/tamagui.config.ts
Normal file
139
apps/expo/tamagui.config.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { createAnimations } from "@tamagui/animations-moti";
|
||||||
|
import { config } from "@tamagui/config/v3";
|
||||||
|
import { createTamagui } from "tamagui";
|
||||||
|
|
||||||
|
import {
|
||||||
|
blueTokens,
|
||||||
|
grayTokens,
|
||||||
|
mainTokens,
|
||||||
|
redTokens,
|
||||||
|
tealTokens,
|
||||||
|
} from "@movie-web/colors";
|
||||||
|
|
||||||
|
type Tokens =
|
||||||
|
| typeof mainTokens
|
||||||
|
| typeof blueTokens
|
||||||
|
| typeof grayTokens
|
||||||
|
| typeof redTokens
|
||||||
|
| typeof tealTokens;
|
||||||
|
|
||||||
|
const createThemeConfig = (tokens: Tokens) => ({
|
||||||
|
screenBackground: tokens.shade.c900,
|
||||||
|
|
||||||
|
searchIcon: tokens.shade.c100,
|
||||||
|
searchBackground: tokens.shade.c600,
|
||||||
|
searchHoverBackground: tokens.shade.c600,
|
||||||
|
searchFocused: tokens.shade.c400,
|
||||||
|
searchPlaceholder: tokens.shade.c100,
|
||||||
|
|
||||||
|
tabBarBackground: tokens.shade.c700,
|
||||||
|
tabBarIcon: tokens.shade.c300,
|
||||||
|
tabBarIconFocused: tokens.purple.c200,
|
||||||
|
|
||||||
|
scrapingCard: tokens.shade.c700,
|
||||||
|
scrapingLoading: tokens.purple.c200,
|
||||||
|
scrapingNoResult: tokens.ash.c100,
|
||||||
|
scrapingError: tokens.semantic.red.c200,
|
||||||
|
scrapingSuccess: tokens.semantic.green.c200,
|
||||||
|
|
||||||
|
playerSettingsBackground: tokens.ash.c900,
|
||||||
|
playerSettingsUnactiveText: tokens.semantic.silver.c400,
|
||||||
|
playerSettingsActiveText: tokens.shade.c100,
|
||||||
|
|
||||||
|
subtitleSelectorBackground: tokens.ash.c500,
|
||||||
|
|
||||||
|
pillBackground: tokens.shade.c300,
|
||||||
|
pillHighlight: tokens.blue.c200,
|
||||||
|
pillActiveBackground: tokens.shade.c300,
|
||||||
|
|
||||||
|
sheetBackground: tokens.shade.c800,
|
||||||
|
sheetItemBackground: tokens.shade.c600,
|
||||||
|
sheetIcon: tokens.shade.c300,
|
||||||
|
sheetText: tokens.shade.c100,
|
||||||
|
sheetHandle: tokens.shade.c300,
|
||||||
|
sheetItemSelected: tokens.purple.c200,
|
||||||
|
|
||||||
|
videoSlider: tokens.ash.c50,
|
||||||
|
videoSliderFilled: tokens.purple.c200,
|
||||||
|
|
||||||
|
progressBackground: tokens.shade.c600,
|
||||||
|
progressFilled: tokens.purple.c200,
|
||||||
|
|
||||||
|
separatorBackground: tokens.ash.c600,
|
||||||
|
|
||||||
|
loadingIndicator: tokens.purple.c200,
|
||||||
|
|
||||||
|
buttonSecondaryBackground: tokens.ash.c700,
|
||||||
|
buttonSecondaryText: tokens.semantic.silver.c300,
|
||||||
|
buttonSecondaryBackgroundHover: tokens.ash.c700,
|
||||||
|
buttonPrimaryBackground: tokens.white,
|
||||||
|
buttonPrimaryText: tokens.black,
|
||||||
|
buttonPrimaryBackgroundHover: tokens.semantic.silver.c100,
|
||||||
|
buttonPurpleBackground: tokens.purple.c500,
|
||||||
|
buttonPurpleBackgroundHover: tokens.purple.c400,
|
||||||
|
buttonCancelBackground: tokens.ash.c500,
|
||||||
|
buttonCancelBackgroundHover: tokens.ash.c300,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const tamaguiConfig = createTamagui({
|
||||||
|
...config,
|
||||||
|
tokens: config.tokens,
|
||||||
|
themes: {
|
||||||
|
main: {
|
||||||
|
...config.themes.dark,
|
||||||
|
...createThemeConfig(mainTokens),
|
||||||
|
},
|
||||||
|
blue: {
|
||||||
|
...config.themes.dark,
|
||||||
|
...createThemeConfig(blueTokens),
|
||||||
|
},
|
||||||
|
gray: {
|
||||||
|
...config.themes.dark,
|
||||||
|
...createThemeConfig(grayTokens),
|
||||||
|
},
|
||||||
|
red: {
|
||||||
|
...config.themes.dark,
|
||||||
|
...createThemeConfig(redTokens),
|
||||||
|
},
|
||||||
|
teal: {
|
||||||
|
...config.themes.dark,
|
||||||
|
...createThemeConfig(tealTokens),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animations: createAnimations({
|
||||||
|
fast: {
|
||||||
|
type: "spring",
|
||||||
|
damping: 20,
|
||||||
|
mass: 1.2,
|
||||||
|
stiffness: 250,
|
||||||
|
},
|
||||||
|
bounce: {
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 200,
|
||||||
|
damping: 10,
|
||||||
|
},
|
||||||
|
quicker: {
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 300,
|
||||||
|
damping: 20,
|
||||||
|
},
|
||||||
|
static: {
|
||||||
|
type: "decay",
|
||||||
|
deceleration: 0.999,
|
||||||
|
},
|
||||||
|
lazy: {
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 100,
|
||||||
|
damping: 20,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tamaguiConfig;
|
||||||
|
|
||||||
|
export type Conf = typeof tamaguiConfig;
|
||||||
|
|
||||||
|
declare module "tamagui" {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||||
|
interface TamaguiCustomConfig extends Conf {}
|
||||||
|
}
|
@@ -9,7 +9,6 @@
|
|||||||
},
|
},
|
||||||
"jsx": "react-native",
|
"jsx": "react-native",
|
||||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json",
|
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json",
|
||||||
"types": ["nativewind/types"],
|
|
||||||
},
|
},
|
||||||
"include": ["src", "*.ts", "*.js", ".expo/types/**/*.ts", "expo-env.d.ts"],
|
"include": ["src", "*.ts", "*.js", ".expo/types/**/*.ts", "expo-env.d.ts"],
|
||||||
"exclude": ["node_modules"],
|
"exclude": ["node_modules"],
|
||||||
|
3293
pnpm-lock.yaml
generated
3293
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "@movie-web/tailwind-config",
|
"name": "@movie-web/colors",
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"main": "./src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
"./native": "./native.ts",
|
".": "./src/index.ts"
|
||||||
"./themes": "./themes/index.ts"
|
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -14,13 +14,6 @@
|
|||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
|
||||||
"autoprefixer": "^10.4.17",
|
|
||||||
"postcss": "^8.4.32",
|
|
||||||
"tailwindcss": "^3.4.0",
|
|
||||||
"tailwindcss-animate": "^1.0.7",
|
|
||||||
"tailwindcss-themer": "^4.0.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@movie-web/eslint-config": "workspace:^0.2.0",
|
"@movie-web/eslint-config": "workspace:^0.2.0",
|
||||||
"@movie-web/prettier-config": "workspace:^0.1.0",
|
"@movie-web/prettier-config": "workspace:^0.1.0",
|
||||||
@@ -30,7 +23,6 @@
|
|||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"root": true,
|
|
||||||
"extends": [
|
"extends": [
|
||||||
"@movie-web/eslint-config/base"
|
"@movie-web/eslint-config/base"
|
||||||
]
|
]
|
93
tooling/colors/src/index.ts
Normal file
93
tooling/colors/src/index.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { tokens as blueTokens } from "./list/blue";
|
||||||
|
import { tokens as grayTokens } from "./list/gray";
|
||||||
|
import { tokens as redTokens } from "./list/red";
|
||||||
|
import { tokens as tealTokens } from "./list/teal";
|
||||||
|
|
||||||
|
export { blueTokens, grayTokens, redTokens, tealTokens };
|
||||||
|
|
||||||
|
export const name = "colors";
|
||||||
|
|
||||||
|
export const mainTokens = {
|
||||||
|
black: "#000000",
|
||||||
|
white: "#FFFFFF",
|
||||||
|
semantic: {
|
||||||
|
red: {
|
||||||
|
c100: "#F46E6E",
|
||||||
|
c200: "#E44F4F",
|
||||||
|
c300: "#D74747",
|
||||||
|
c400: "#B43434",
|
||||||
|
},
|
||||||
|
green: {
|
||||||
|
c100: "#60D26A",
|
||||||
|
c200: "#40B44B",
|
||||||
|
c300: "#31A33C",
|
||||||
|
c400: "#237A2B",
|
||||||
|
},
|
||||||
|
silver: {
|
||||||
|
c100: "#DEDEDE",
|
||||||
|
c200: "#B6CAD7",
|
||||||
|
c300: "#8EA3B0",
|
||||||
|
c400: "#617A8A",
|
||||||
|
},
|
||||||
|
yellow: {
|
||||||
|
c100: "#FFF599",
|
||||||
|
c200: "#FCEC61",
|
||||||
|
c300: "#D8C947",
|
||||||
|
c400: "#AFA349",
|
||||||
|
},
|
||||||
|
rose: {
|
||||||
|
c100: "#DB3D61",
|
||||||
|
c200: "#8A293B",
|
||||||
|
c300: "#812435",
|
||||||
|
c400: "#701B2B",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
blue: {
|
||||||
|
c50: "#ADADF5",
|
||||||
|
c100: "#7979CC",
|
||||||
|
c200: "#5D5DAE",
|
||||||
|
c300: "#3B3B8C",
|
||||||
|
c400: "#2A2A71",
|
||||||
|
c500: "#1F1F50",
|
||||||
|
c600: "#1B1B41",
|
||||||
|
c700: "#171736",
|
||||||
|
c800: "#101020",
|
||||||
|
c900: "#0B0B13",
|
||||||
|
},
|
||||||
|
purple: {
|
||||||
|
c50: "#D5AAFF",
|
||||||
|
c100: "#C082FF",
|
||||||
|
c200: "#A359EC",
|
||||||
|
c300: "#8D44D6",
|
||||||
|
c400: "#7831BF",
|
||||||
|
c500: "#572887",
|
||||||
|
c600: "#411F64",
|
||||||
|
c700: "#31184A",
|
||||||
|
c800: "#221134",
|
||||||
|
c900: "#160B22",
|
||||||
|
},
|
||||||
|
ash: {
|
||||||
|
c50: "#7F8D9B",
|
||||||
|
c100: "#5B6B7B",
|
||||||
|
c200: "#445464",
|
||||||
|
c300: "#2B3D4E",
|
||||||
|
c400: "#203242",
|
||||||
|
c500: "#1C2C3C",
|
||||||
|
c600: "#172532",
|
||||||
|
c700: "#131E29",
|
||||||
|
c800: "#101820",
|
||||||
|
c900: "#0C1216",
|
||||||
|
},
|
||||||
|
shade: {
|
||||||
|
c50: "#676790",
|
||||||
|
c100: "#52527A",
|
||||||
|
c200: "#3F3F60",
|
||||||
|
c300: "#32324F",
|
||||||
|
c400: "#272741",
|
||||||
|
c500: "#1E1E32",
|
||||||
|
c600: "#171728",
|
||||||
|
c700: "#131322",
|
||||||
|
c800: "#0F0F1B",
|
||||||
|
c900: "#0A0A12",
|
||||||
|
},
|
||||||
|
};
|
84
tooling/colors/src/list/blue.ts
Normal file
84
tooling/colors/src/list/blue.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
export const tokens = {
|
||||||
|
black: "#000000",
|
||||||
|
white: "#FFFFFF",
|
||||||
|
semantic: {
|
||||||
|
red: {
|
||||||
|
c100: "#F46E6E",
|
||||||
|
c200: "#E44F4F",
|
||||||
|
c300: "#D74747",
|
||||||
|
c400: "#B43434",
|
||||||
|
},
|
||||||
|
green: {
|
||||||
|
c100: "#60D26A",
|
||||||
|
c200: "#40B44B",
|
||||||
|
c300: "#31A33C",
|
||||||
|
c400: "#237A2B",
|
||||||
|
},
|
||||||
|
silver: {
|
||||||
|
c100: "#DEDEDE",
|
||||||
|
c200: "#B6CAD7",
|
||||||
|
c300: "#8EA3B0",
|
||||||
|
c400: "#617A8A",
|
||||||
|
},
|
||||||
|
yellow: {
|
||||||
|
c100: "#FFF599",
|
||||||
|
c200: "#FCEC61",
|
||||||
|
c300: "#D8C947",
|
||||||
|
c400: "#AFA349",
|
||||||
|
},
|
||||||
|
rose: {
|
||||||
|
c100: "#DB3D61",
|
||||||
|
c200: "#8A293B",
|
||||||
|
c300: "#812435",
|
||||||
|
c400: "#701B2B",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
purple: {
|
||||||
|
c50: "#aac5ff",
|
||||||
|
c100: "#82a9ff",
|
||||||
|
c200: "#4681ff",
|
||||||
|
c300: "#1a63ff",
|
||||||
|
c400: "#054eec",
|
||||||
|
c500: "#083aa7",
|
||||||
|
c600: "#072c7c",
|
||||||
|
c700: "#06215d",
|
||||||
|
c800: "#041741",
|
||||||
|
c900: "#03102a",
|
||||||
|
},
|
||||||
|
shade: {
|
||||||
|
c50: "#756790",
|
||||||
|
c100: "#60527a",
|
||||||
|
c200: "#4a3f60",
|
||||||
|
c300: "#3c324f",
|
||||||
|
c400: "#302741",
|
||||||
|
c500: "#251e32",
|
||||||
|
c600: "#1d1728",
|
||||||
|
c700: "#181322",
|
||||||
|
c800: "#130f1b",
|
||||||
|
c900: "#0d0a12",
|
||||||
|
},
|
||||||
|
ash: {
|
||||||
|
c50: "#7f859b",
|
||||||
|
c100: "#5b627b",
|
||||||
|
c200: "#444b64",
|
||||||
|
c300: "#2b344e",
|
||||||
|
c400: "#202842",
|
||||||
|
c500: "#1c243c",
|
||||||
|
c600: "#171d32",
|
||||||
|
c700: "#131829",
|
||||||
|
c800: "#101420",
|
||||||
|
c900: "#0c0f16",
|
||||||
|
},
|
||||||
|
blue: {
|
||||||
|
c50: "#adb4f5",
|
||||||
|
c100: "#7981cc",
|
||||||
|
c200: "#5d65ae",
|
||||||
|
c300: "#3b438c",
|
||||||
|
c400: "#2a3171",
|
||||||
|
c500: "#1f2450",
|
||||||
|
c600: "#1b1f41",
|
||||||
|
c700: "#171b36",
|
||||||
|
c800: "#101120",
|
||||||
|
c900: "#0b0c13",
|
||||||
|
},
|
||||||
|
};
|
84
tooling/colors/src/list/gray.ts
Normal file
84
tooling/colors/src/list/gray.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
export const tokens = {
|
||||||
|
black: "#000000",
|
||||||
|
white: "#FFFFFF",
|
||||||
|
semantic: {
|
||||||
|
red: {
|
||||||
|
c100: "#F46E6E",
|
||||||
|
c200: "#E44F4F",
|
||||||
|
c300: "#D74747",
|
||||||
|
c400: "#B43434",
|
||||||
|
},
|
||||||
|
green: {
|
||||||
|
c100: "#60D26A",
|
||||||
|
c200: "#40B44B",
|
||||||
|
c300: "#31A33C",
|
||||||
|
c400: "#237A2B",
|
||||||
|
},
|
||||||
|
silver: {
|
||||||
|
c100: "#DEDEDE",
|
||||||
|
c200: "#B6CAD7",
|
||||||
|
c300: "#8EA3B0",
|
||||||
|
c400: "#617A8A",
|
||||||
|
},
|
||||||
|
yellow: {
|
||||||
|
c100: "#FFF599",
|
||||||
|
c200: "#FCEC61",
|
||||||
|
c300: "#D8C947",
|
||||||
|
c400: "#AFA349",
|
||||||
|
},
|
||||||
|
rose: {
|
||||||
|
c100: "#DB3D61",
|
||||||
|
c200: "#8A293B",
|
||||||
|
c300: "#812435",
|
||||||
|
c400: "#701B2B",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
purple: {
|
||||||
|
c50: "#aaafff",
|
||||||
|
c100: "#8288fe",
|
||||||
|
c200: "#5a62eb",
|
||||||
|
c300: "#454cd4",
|
||||||
|
c400: "#333abe",
|
||||||
|
c500: "#292d86",
|
||||||
|
c600: "#1f2363",
|
||||||
|
c700: "#191b4a",
|
||||||
|
c800: "#111334",
|
||||||
|
c900: "#0b0d22",
|
||||||
|
},
|
||||||
|
shade: {
|
||||||
|
c50: "#7c7c7c",
|
||||||
|
c100: "#666666",
|
||||||
|
c200: "#4f4f4f",
|
||||||
|
c300: "#404040",
|
||||||
|
c400: "#343434",
|
||||||
|
c500: "#282828",
|
||||||
|
c600: "#202020",
|
||||||
|
c700: "#1a1a1a",
|
||||||
|
c800: "#151515",
|
||||||
|
c900: "#0e0e0e",
|
||||||
|
},
|
||||||
|
ash: {
|
||||||
|
c50: "#8d8d8d",
|
||||||
|
c100: "#6b6b6b",
|
||||||
|
c200: "#545454",
|
||||||
|
c300: "#3c3c3c",
|
||||||
|
c400: "#313131",
|
||||||
|
c500: "#2c2c2c",
|
||||||
|
c600: "#252525",
|
||||||
|
c700: "#1e1e1e",
|
||||||
|
c800: "#181818",
|
||||||
|
c900: "#111111",
|
||||||
|
},
|
||||||
|
blue: {
|
||||||
|
c50: "#ccccd6",
|
||||||
|
c100: "#a2a2a2",
|
||||||
|
c200: "#868686",
|
||||||
|
c300: "#646464",
|
||||||
|
c400: "#4e4e4e",
|
||||||
|
c500: "#383838",
|
||||||
|
c600: "#2e2e2e",
|
||||||
|
c700: "#272727",
|
||||||
|
c800: "#181818",
|
||||||
|
c900: "#0f0f0f",
|
||||||
|
},
|
||||||
|
};
|
84
tooling/colors/src/list/red.ts
Normal file
84
tooling/colors/src/list/red.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
export const tokens = {
|
||||||
|
black: "#000000",
|
||||||
|
white: "#FFFFFF",
|
||||||
|
semantic: {
|
||||||
|
red: {
|
||||||
|
c100: "#F46E6E",
|
||||||
|
c200: "#E44F4F",
|
||||||
|
c300: "#D74747",
|
||||||
|
c400: "#B43434",
|
||||||
|
},
|
||||||
|
green: {
|
||||||
|
c100: "#60D26A",
|
||||||
|
c200: "#40B44B",
|
||||||
|
c300: "#31A33C",
|
||||||
|
c400: "#237A2B",
|
||||||
|
},
|
||||||
|
silver: {
|
||||||
|
c100: "#DEDEDE",
|
||||||
|
c200: "#B6CAD7",
|
||||||
|
c300: "#8EA3B0",
|
||||||
|
c400: "#617A8A",
|
||||||
|
},
|
||||||
|
yellow: {
|
||||||
|
c100: "#FFF599",
|
||||||
|
c200: "#FCEC61",
|
||||||
|
c300: "#D8C947",
|
||||||
|
c400: "#AFA349",
|
||||||
|
},
|
||||||
|
rose: {
|
||||||
|
c100: "#DB3D61",
|
||||||
|
c200: "#8A293B",
|
||||||
|
c300: "#812435",
|
||||||
|
c400: "#701B2B",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
purple: {
|
||||||
|
c50: "#feabac",
|
||||||
|
c100: "#fe8385",
|
||||||
|
c200: "#ea5b5e",
|
||||||
|
c300: "#d34648",
|
||||||
|
c400: "#bd3436",
|
||||||
|
c500: "#852a2b",
|
||||||
|
c600: "#632021",
|
||||||
|
c700: "#49191a",
|
||||||
|
c800: "#331112",
|
||||||
|
c900: "#220c0c",
|
||||||
|
},
|
||||||
|
shade: {
|
||||||
|
c50: "#9c605c",
|
||||||
|
c100: "#834d49",
|
||||||
|
c200: "#673b38",
|
||||||
|
c300: "#542f2c",
|
||||||
|
c400: "#452422",
|
||||||
|
c500: "#361c1a",
|
||||||
|
c600: "#2b1614",
|
||||||
|
c700: "#241210",
|
||||||
|
c800: "#1c0e0d",
|
||||||
|
c900: "#130909",
|
||||||
|
},
|
||||||
|
ash: {
|
||||||
|
c50: "#ac6e6f",
|
||||||
|
c100: "#8b4b4c",
|
||||||
|
c200: "#703739",
|
||||||
|
c300: "#572225",
|
||||||
|
c400: "#49191a",
|
||||||
|
c500: "#421617",
|
||||||
|
c600: "#371212",
|
||||||
|
c700: "#2e0e0f",
|
||||||
|
c800: "#230c0d",
|
||||||
|
c900: "#19090b",
|
||||||
|
},
|
||||||
|
blue: {
|
||||||
|
c50: "#f5adb4",
|
||||||
|
c100: "#cc7981",
|
||||||
|
c200: "#ae5d65",
|
||||||
|
c300: "#8c3b43",
|
||||||
|
c400: "#712a31",
|
||||||
|
c500: "#501f24",
|
||||||
|
c600: "#411b1f",
|
||||||
|
c700: "#36171b",
|
||||||
|
c800: "#201011",
|
||||||
|
c900: "#130b0c",
|
||||||
|
},
|
||||||
|
};
|
84
tooling/colors/src/list/teal.ts
Normal file
84
tooling/colors/src/list/teal.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
export const tokens = {
|
||||||
|
black: "#000000",
|
||||||
|
white: "#FFFFFF",
|
||||||
|
semantic: {
|
||||||
|
red: {
|
||||||
|
c100: "#F46E6E",
|
||||||
|
c200: "#E44F4F",
|
||||||
|
c300: "#D74747",
|
||||||
|
c400: "#B43434",
|
||||||
|
},
|
||||||
|
green: {
|
||||||
|
c100: "#60D26A",
|
||||||
|
c200: "#40B44B",
|
||||||
|
c300: "#31A33C",
|
||||||
|
c400: "#237A2B",
|
||||||
|
},
|
||||||
|
silver: {
|
||||||
|
c100: "#DEDEDE",
|
||||||
|
c200: "#B6CAD7",
|
||||||
|
c300: "#8EA3B0",
|
||||||
|
c400: "#617A8A",
|
||||||
|
},
|
||||||
|
yellow: {
|
||||||
|
c100: "#FFF599",
|
||||||
|
c200: "#FCEC61",
|
||||||
|
c300: "#D8C947",
|
||||||
|
c400: "#AFA349",
|
||||||
|
},
|
||||||
|
rose: {
|
||||||
|
c100: "#DB3D61",
|
||||||
|
c200: "#8A293B",
|
||||||
|
c300: "#812435",
|
||||||
|
c400: "#701B2B",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
purple: {
|
||||||
|
c50: "#aad7ff",
|
||||||
|
c100: "#82c4ff",
|
||||||
|
c200: "#59a8ec",
|
||||||
|
c300: "#4491d6",
|
||||||
|
c400: "#317dbf",
|
||||||
|
c500: "#285b87",
|
||||||
|
c600: "#1f4464",
|
||||||
|
c700: "#18334a",
|
||||||
|
c800: "#112434",
|
||||||
|
c900: "#0b1822",
|
||||||
|
},
|
||||||
|
shade: {
|
||||||
|
c50: "#677c90",
|
||||||
|
c100: "#52667a",
|
||||||
|
c200: "#3f4f60",
|
||||||
|
c300: "#32404f",
|
||||||
|
c400: "#273441",
|
||||||
|
c500: "#1e2832",
|
||||||
|
c600: "#172028",
|
||||||
|
c700: "#131a22",
|
||||||
|
c800: "#0f151b",
|
||||||
|
c900: "#0a0e12",
|
||||||
|
},
|
||||||
|
ash: {
|
||||||
|
c50: "#7f9b9b",
|
||||||
|
c100: "#5b7b7b",
|
||||||
|
c200: "#446463",
|
||||||
|
c300: "#2b4e4d",
|
||||||
|
c400: "#204241",
|
||||||
|
c500: "#1c3c3b",
|
||||||
|
c600: "#173232",
|
||||||
|
c700: "#132929",
|
||||||
|
c800: "#102020",
|
||||||
|
c900: "#0c1615",
|
||||||
|
},
|
||||||
|
blue: {
|
||||||
|
c50: "#adf5d6",
|
||||||
|
c100: "#79cca8",
|
||||||
|
c200: "#5dae8b",
|
||||||
|
c300: "#3b8c69",
|
||||||
|
c400: "#2a7152",
|
||||||
|
c500: "#1f503b",
|
||||||
|
c600: "#1b4130",
|
||||||
|
c700: "#173629",
|
||||||
|
c800: "#102019",
|
||||||
|
c900: "#0b1310",
|
||||||
|
},
|
||||||
|
};
|
8
tooling/colors/tsconfig.json
Normal file
8
tooling/colors/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "@movie-web/tsconfig/base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||||
|
},
|
||||||
|
"include": ["*.ts", "src"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
@@ -30,6 +30,7 @@ const config = {
|
|||||||
],
|
],
|
||||||
"import/consistent-type-specifier-style": ["error", "prefer-top-level"],
|
"import/consistent-type-specifier-style": ["error", "prefer-top-level"],
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-argument": "off",
|
||||||
},
|
},
|
||||||
ignorePatterns: [
|
ignorePatterns: [
|
||||||
"**/*.config.js",
|
"**/*.config.js",
|
||||||
|
@@ -1,19 +1,11 @@
|
|||||||
import { fileURLToPath } from "url";
|
|
||||||
|
|
||||||
/** @typedef {import("prettier").Config} PrettierConfig */
|
/** @typedef {import("prettier").Config} PrettierConfig */
|
||||||
/** @typedef {import("prettier-plugin-tailwindcss").PluginOptions} TailwindConfig */
|
|
||||||
/** @typedef {import("@ianvs/prettier-plugin-sort-imports").PluginConfig} SortImportsConfig */
|
/** @typedef {import("@ianvs/prettier-plugin-sort-imports").PluginConfig} SortImportsConfig */
|
||||||
|
|
||||||
/** @type { PrettierConfig | SortImportsConfig | TailwindConfig } */
|
/** @type { PrettierConfig | SortImportsConfig } */
|
||||||
const config = {
|
const config = {
|
||||||
plugins: [
|
plugins: [
|
||||||
"@ianvs/prettier-plugin-sort-imports",
|
"@ianvs/prettier-plugin-sort-imports",
|
||||||
"prettier-plugin-tailwindcss",
|
|
||||||
],
|
],
|
||||||
tailwindConfig: fileURLToPath(
|
|
||||||
new URL("../../tooling/tailwind/native.ts", import.meta.url),
|
|
||||||
),
|
|
||||||
tailwindFunctions: ["cn", "cva"],
|
|
||||||
importOrder: [
|
importOrder: [
|
||||||
"<TYPES>",
|
"<TYPES>",
|
||||||
"^(react/(.*)$)|^(react$)|^(react-native(.*)$)",
|
"^(react/(.*)$)|^(react$)|^(react-native(.*)$)",
|
||||||
|
@@ -1,22 +0,0 @@
|
|||||||
import type { Config } from "tailwindcss";
|
|
||||||
import themer from "tailwindcss-themer";
|
|
||||||
|
|
||||||
import { allThemes, defaultTheme, safeThemeList } from "./themes";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
content: ["src/**/*.{ts,tsx}"],
|
|
||||||
safelist: safeThemeList,
|
|
||||||
plugins: [
|
|
||||||
themer({
|
|
||||||
defaultTheme,
|
|
||||||
themes: [
|
|
||||||
{
|
|
||||||
name: "default",
|
|
||||||
selectors: [".theme-default"],
|
|
||||||
...defaultTheme,
|
|
||||||
},
|
|
||||||
...allThemes,
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
} satisfies Config;
|
|
@@ -1,6 +0,0 @@
|
|||||||
import blue from "./list/blue";
|
|
||||||
import gray from "./list/gray";
|
|
||||||
import red from "./list/red";
|
|
||||||
import teal from "./list/teal";
|
|
||||||
|
|
||||||
export const allThemes = [teal, blue, gray, red];
|
|
@@ -1,352 +0,0 @@
|
|||||||
const tokens = {
|
|
||||||
black: "#000000",
|
|
||||||
white: "#FFFFFF",
|
|
||||||
semantic: {
|
|
||||||
red: {
|
|
||||||
c100: "#F46E6E",
|
|
||||||
c200: "#E44F4F",
|
|
||||||
c300: "#D74747",
|
|
||||||
c400: "#B43434",
|
|
||||||
},
|
|
||||||
green: {
|
|
||||||
c100: "#60D26A",
|
|
||||||
c200: "#40B44B",
|
|
||||||
c300: "#31A33C",
|
|
||||||
c400: "#237A2B",
|
|
||||||
},
|
|
||||||
silver: {
|
|
||||||
c100: "#DEDEDE",
|
|
||||||
c200: "#B6CAD7",
|
|
||||||
c300: "#8EA3B0",
|
|
||||||
c400: "#617A8A",
|
|
||||||
},
|
|
||||||
yellow: {
|
|
||||||
c100: "#FFF599",
|
|
||||||
c200: "#FCEC61",
|
|
||||||
c300: "#D8C947",
|
|
||||||
c400: "#AFA349",
|
|
||||||
},
|
|
||||||
rose: {
|
|
||||||
c100: "#DB3D61",
|
|
||||||
c200: "#8A293B",
|
|
||||||
c300: "#812435",
|
|
||||||
c400: "#701B2B",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
blue: {
|
|
||||||
c50: "#ADADF5",
|
|
||||||
c100: "#7979CC",
|
|
||||||
c200: "#5D5DAE",
|
|
||||||
c300: "#3B3B8C",
|
|
||||||
c400: "#2A2A71",
|
|
||||||
c500: "#1F1F50",
|
|
||||||
c600: "#1B1B41",
|
|
||||||
c700: "#171736",
|
|
||||||
c800: "#101020",
|
|
||||||
c900: "#0B0B13",
|
|
||||||
},
|
|
||||||
purple: {
|
|
||||||
c50: "#D5AAFF",
|
|
||||||
c100: "#C082FF",
|
|
||||||
c200: "#A359EC",
|
|
||||||
c300: "#8D44D6",
|
|
||||||
c400: "#7831BF",
|
|
||||||
c500: "#572887",
|
|
||||||
c600: "#411F64",
|
|
||||||
c700: "#31184A",
|
|
||||||
c800: "#221134",
|
|
||||||
c900: "#160B22",
|
|
||||||
},
|
|
||||||
ash: {
|
|
||||||
c50: "#7F8D9B",
|
|
||||||
c100: "#5B6B7B",
|
|
||||||
c200: "#445464",
|
|
||||||
c300: "#2B3D4E",
|
|
||||||
c400: "#203242",
|
|
||||||
c500: "#1C2C3C",
|
|
||||||
c600: "#172532",
|
|
||||||
c700: "#131E29",
|
|
||||||
c800: "#101820",
|
|
||||||
c900: "#0C1216",
|
|
||||||
},
|
|
||||||
shade: {
|
|
||||||
c50: "#676790",
|
|
||||||
c100: "#52527A",
|
|
||||||
c200: "#3F3F60",
|
|
||||||
c300: "#32324F",
|
|
||||||
c400: "#272741",
|
|
||||||
c500: "#1E1E32",
|
|
||||||
c600: "#171728",
|
|
||||||
c700: "#131322",
|
|
||||||
c800: "#0F0F1B",
|
|
||||||
c900: "#0A0A12",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const defaultTheme = {
|
|
||||||
extend: {
|
|
||||||
colors: {
|
|
||||||
themePreview: {
|
|
||||||
primary: tokens.blue.c200,
|
|
||||||
secondary: tokens.shade.c50,
|
|
||||||
ghost: tokens.white,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Branding
|
|
||||||
pill: {
|
|
||||||
background: tokens.shade.c300,
|
|
||||||
backgroundHover: tokens.shade.c200,
|
|
||||||
highlight: tokens.blue.c200,
|
|
||||||
|
|
||||||
activeBackground: tokens.shade.c300,
|
|
||||||
},
|
|
||||||
|
|
||||||
// meta data for the theme itself
|
|
||||||
global: {
|
|
||||||
accentA: tokens.blue.c200,
|
|
||||||
accentB: tokens.blue.c300,
|
|
||||||
},
|
|
||||||
|
|
||||||
// light bar
|
|
||||||
lightBar: {
|
|
||||||
light: tokens.blue.c400,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Buttons
|
|
||||||
buttons: {
|
|
||||||
toggle: tokens.purple.c300,
|
|
||||||
toggleDisabled: tokens.ash.c500,
|
|
||||||
danger: tokens.semantic.rose.c300,
|
|
||||||
dangerHover: tokens.semantic.rose.c200,
|
|
||||||
|
|
||||||
secondary: tokens.ash.c700,
|
|
||||||
secondaryText: tokens.semantic.silver.c300,
|
|
||||||
secondaryHover: tokens.ash.c700,
|
|
||||||
primary: tokens.white,
|
|
||||||
primaryText: tokens.black,
|
|
||||||
primaryHover: tokens.semantic.silver.c100,
|
|
||||||
purple: tokens.purple.c500,
|
|
||||||
purpleHover: tokens.purple.c400,
|
|
||||||
cancel: tokens.ash.c500,
|
|
||||||
cancelHover: tokens.ash.c300,
|
|
||||||
},
|
|
||||||
|
|
||||||
// only used for body colors/textures
|
|
||||||
background: {
|
|
||||||
main: tokens.shade.c900,
|
|
||||||
secondary: tokens.shade.c600,
|
|
||||||
secondaryHover: tokens.shade.c400,
|
|
||||||
accentA: tokens.purple.c500,
|
|
||||||
accentB: tokens.blue.c500,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Modals
|
|
||||||
modal: {
|
|
||||||
background: tokens.shade.c800,
|
|
||||||
},
|
|
||||||
|
|
||||||
// typography
|
|
||||||
type: {
|
|
||||||
logo: tokens.purple.c100,
|
|
||||||
emphasis: tokens.white,
|
|
||||||
text: tokens.shade.c50,
|
|
||||||
dimmed: tokens.shade.c50,
|
|
||||||
divider: tokens.ash.c500,
|
|
||||||
secondary: tokens.ash.c100,
|
|
||||||
danger: tokens.semantic.red.c100,
|
|
||||||
success: tokens.semantic.green.c100,
|
|
||||||
link: tokens.purple.c100,
|
|
||||||
linkHover: tokens.purple.c50,
|
|
||||||
},
|
|
||||||
|
|
||||||
// search bar
|
|
||||||
search: {
|
|
||||||
background: tokens.shade.c500,
|
|
||||||
hoverBackground: tokens.shade.c600,
|
|
||||||
focused: tokens.shade.c400,
|
|
||||||
placeholder: tokens.shade.c100,
|
|
||||||
icon: tokens.shade.c100,
|
|
||||||
text: tokens.white,
|
|
||||||
},
|
|
||||||
|
|
||||||
// media cards
|
|
||||||
mediaCard: {
|
|
||||||
hoverBackground: tokens.shade.c600,
|
|
||||||
hoverAccent: tokens.shade.c50,
|
|
||||||
hoverShadow: tokens.shade.c900,
|
|
||||||
shadow: tokens.shade.c700,
|
|
||||||
barColor: tokens.ash.c200,
|
|
||||||
barFillColor: tokens.purple.c100,
|
|
||||||
badge: tokens.shade.c700,
|
|
||||||
badgeText: tokens.ash.c100,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Large card
|
|
||||||
largeCard: {
|
|
||||||
background: tokens.shade.c600,
|
|
||||||
icon: tokens.purple.c400,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Dropdown
|
|
||||||
dropdown: {
|
|
||||||
background: tokens.shade.c600,
|
|
||||||
altBackground: tokens.shade.c700,
|
|
||||||
hoverBackground: tokens.shade.c500,
|
|
||||||
highlight: tokens.semantic.yellow.c400,
|
|
||||||
highlightHover: tokens.semantic.yellow.c200,
|
|
||||||
text: tokens.shade.c50,
|
|
||||||
secondary: tokens.shade.c100,
|
|
||||||
border: tokens.shade.c400,
|
|
||||||
contentBackground: tokens.shade.c500,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Passphrase
|
|
||||||
authentication: {
|
|
||||||
border: tokens.shade.c300,
|
|
||||||
inputBg: tokens.shade.c600,
|
|
||||||
inputBgHover: tokens.shade.c500,
|
|
||||||
wordBackground: tokens.shade.c500,
|
|
||||||
copyText: tokens.shade.c100,
|
|
||||||
copyTextHover: tokens.ash.c50,
|
|
||||||
errorText: tokens.semantic.rose.c100,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Settings page
|
|
||||||
settings: {
|
|
||||||
sidebar: {
|
|
||||||
activeLink: tokens.shade.c600,
|
|
||||||
badge: tokens.shade.c900,
|
|
||||||
|
|
||||||
type: {
|
|
||||||
secondary: tokens.shade.c200,
|
|
||||||
inactive: tokens.shade.c50,
|
|
||||||
icon: tokens.shade.c50,
|
|
||||||
iconActivated: tokens.purple.c200,
|
|
||||||
activated: tokens.purple.c50,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
card: {
|
|
||||||
border: tokens.shade.c400,
|
|
||||||
background: tokens.shade.c400,
|
|
||||||
altBackground: tokens.shade.c400,
|
|
||||||
},
|
|
||||||
|
|
||||||
saveBar: {
|
|
||||||
background: tokens.shade.c800,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// Utilities
|
|
||||||
utils: {
|
|
||||||
divider: tokens.ash.c300,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Onboarding
|
|
||||||
onboarding: {
|
|
||||||
bar: tokens.shade.c400,
|
|
||||||
barFilled: tokens.purple.c300,
|
|
||||||
divider: tokens.shade.c200,
|
|
||||||
card: tokens.shade.c800,
|
|
||||||
cardHover: tokens.shade.c700,
|
|
||||||
border: tokens.shade.c600,
|
|
||||||
good: tokens.purple.c100,
|
|
||||||
best: tokens.semantic.yellow.c100,
|
|
||||||
link: tokens.purple.c100,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Error page
|
|
||||||
errors: {
|
|
||||||
card: tokens.shade.c800,
|
|
||||||
border: tokens.ash.c500,
|
|
||||||
|
|
||||||
type: {
|
|
||||||
secondary: tokens.ash.c100,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// About page
|
|
||||||
about: {
|
|
||||||
circle: tokens.ash.c500,
|
|
||||||
circleText: tokens.ash.c50,
|
|
||||||
},
|
|
||||||
|
|
||||||
// About page
|
|
||||||
editBadge: {
|
|
||||||
bg: tokens.ash.c500,
|
|
||||||
bgHover: tokens.ash.c400,
|
|
||||||
text: tokens.ash.c50,
|
|
||||||
},
|
|
||||||
|
|
||||||
progress: {
|
|
||||||
background: tokens.ash.c50,
|
|
||||||
preloaded: tokens.ash.c50,
|
|
||||||
filled: tokens.purple.c200,
|
|
||||||
},
|
|
||||||
|
|
||||||
// video player
|
|
||||||
video: {
|
|
||||||
buttonBackground: tokens.ash.c200,
|
|
||||||
|
|
||||||
autoPlay: {
|
|
||||||
background: tokens.ash.c700,
|
|
||||||
hover: tokens.ash.c500,
|
|
||||||
},
|
|
||||||
|
|
||||||
scraping: {
|
|
||||||
card: tokens.shade.c700,
|
|
||||||
error: tokens.semantic.red.c200,
|
|
||||||
success: tokens.semantic.green.c200,
|
|
||||||
loading: tokens.purple.c200,
|
|
||||||
noresult: tokens.ash.c100,
|
|
||||||
},
|
|
||||||
|
|
||||||
audio: {
|
|
||||||
set: tokens.purple.c200,
|
|
||||||
},
|
|
||||||
|
|
||||||
context: {
|
|
||||||
background: tokens.ash.c900,
|
|
||||||
light: tokens.shade.c50,
|
|
||||||
border: tokens.ash.c600,
|
|
||||||
hoverColor: tokens.ash.c600,
|
|
||||||
buttonFocus: tokens.ash.c500,
|
|
||||||
flagBg: tokens.ash.c500,
|
|
||||||
inputBg: tokens.ash.c600,
|
|
||||||
buttonOverInputHover: tokens.ash.c500,
|
|
||||||
inputPlaceholder: tokens.ash.c200,
|
|
||||||
cardBorder: tokens.ash.c700,
|
|
||||||
slider: tokens.ash.c50,
|
|
||||||
sliderFilled: tokens.purple.c200,
|
|
||||||
error: tokens.semantic.red.c200,
|
|
||||||
|
|
||||||
buttons: {
|
|
||||||
list: tokens.ash.c700,
|
|
||||||
active: tokens.ash.c900,
|
|
||||||
},
|
|
||||||
|
|
||||||
closeHover: tokens.ash.c800,
|
|
||||||
|
|
||||||
type: {
|
|
||||||
main: tokens.semantic.silver.c400,
|
|
||||||
secondary: tokens.ash.c200,
|
|
||||||
accent: tokens.purple.c200,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// tabBar
|
|
||||||
tabBar: {
|
|
||||||
background: tokens.shade.c700,
|
|
||||||
active: tokens.purple.c200,
|
|
||||||
inactive: tokens.shade.c300,
|
|
||||||
},
|
|
||||||
|
|
||||||
// download
|
|
||||||
download: {
|
|
||||||
progress: tokens.ash.c50,
|
|
||||||
progressFilled: tokens.purple.c200,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
@@ -1,9 +0,0 @@
|
|||||||
import { allThemes } from "./all";
|
|
||||||
|
|
||||||
export { defaultTheme } from "./default";
|
|
||||||
export { allThemes } from "./all";
|
|
||||||
|
|
||||||
export const safeThemeList = allThemes
|
|
||||||
.flatMap((v) => v.selectors)
|
|
||||||
.filter((v) => v.startsWith("."))
|
|
||||||
.map((v) => v.slice(1)); // remove dot from selector
|
|
@@ -1,259 +0,0 @@
|
|||||||
import { createTheme } from "../types";
|
|
||||||
|
|
||||||
const tokens = {
|
|
||||||
purple: {
|
|
||||||
c50: "#aac5ff",
|
|
||||||
c100: "#82a9ff",
|
|
||||||
c200: "#4681ff",
|
|
||||||
c300: "#1a63ff",
|
|
||||||
c400: "#054eec",
|
|
||||||
c500: "#083aa7",
|
|
||||||
c600: "#072c7c",
|
|
||||||
c700: "#06215d",
|
|
||||||
c800: "#041741",
|
|
||||||
c900: "#03102a",
|
|
||||||
},
|
|
||||||
shade: {
|
|
||||||
c50: "#756790",
|
|
||||||
c100: "#60527a",
|
|
||||||
c200: "#4a3f60",
|
|
||||||
c300: "#3c324f",
|
|
||||||
c400: "#302741",
|
|
||||||
c500: "#251e32",
|
|
||||||
c600: "#1d1728",
|
|
||||||
c700: "#181322",
|
|
||||||
c800: "#130f1b",
|
|
||||||
c900: "#0d0a12",
|
|
||||||
},
|
|
||||||
ash: {
|
|
||||||
c50: "#7f859b",
|
|
||||||
c100: "#5b627b",
|
|
||||||
c200: "#444b64",
|
|
||||||
c300: "#2b344e",
|
|
||||||
c400: "#202842",
|
|
||||||
c500: "#1c243c",
|
|
||||||
c600: "#171d32",
|
|
||||||
c700: "#131829",
|
|
||||||
c800: "#101420",
|
|
||||||
c900: "#0c0f16",
|
|
||||||
},
|
|
||||||
blue: {
|
|
||||||
c50: "#adb4f5",
|
|
||||||
c100: "#7981cc",
|
|
||||||
c200: "#5d65ae",
|
|
||||||
c300: "#3b438c",
|
|
||||||
c400: "#2a3171",
|
|
||||||
c500: "#1f2450",
|
|
||||||
c600: "#1b1f41",
|
|
||||||
c700: "#171b36",
|
|
||||||
c800: "#101120",
|
|
||||||
c900: "#0b0c13",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default createTheme({
|
|
||||||
name: "blue",
|
|
||||||
extend: {
|
|
||||||
colors: {
|
|
||||||
themePreview: {
|
|
||||||
primary: tokens.blue.c200,
|
|
||||||
secondary: tokens.shade.c50,
|
|
||||||
},
|
|
||||||
|
|
||||||
pill: {
|
|
||||||
background: tokens.shade.c300,
|
|
||||||
backgroundHover: tokens.shade.c200,
|
|
||||||
highlight: tokens.blue.c200,
|
|
||||||
|
|
||||||
activeBackground: tokens.shade.c300,
|
|
||||||
},
|
|
||||||
|
|
||||||
global: {
|
|
||||||
accentA: tokens.blue.c200,
|
|
||||||
accentB: tokens.blue.c300,
|
|
||||||
},
|
|
||||||
|
|
||||||
lightBar: {
|
|
||||||
light: tokens.blue.c400,
|
|
||||||
},
|
|
||||||
|
|
||||||
buttons: {
|
|
||||||
toggle: tokens.purple.c300,
|
|
||||||
toggleDisabled: tokens.ash.c500,
|
|
||||||
|
|
||||||
secondary: tokens.ash.c700,
|
|
||||||
secondaryHover: tokens.ash.c700,
|
|
||||||
purple: tokens.purple.c500,
|
|
||||||
purpleHover: tokens.purple.c400,
|
|
||||||
cancel: tokens.ash.c500,
|
|
||||||
cancelHover: tokens.ash.c300,
|
|
||||||
},
|
|
||||||
|
|
||||||
background: {
|
|
||||||
main: tokens.shade.c900,
|
|
||||||
secondary: tokens.shade.c600,
|
|
||||||
secondaryHover: tokens.shade.c400,
|
|
||||||
accentA: tokens.purple.c500,
|
|
||||||
accentB: tokens.blue.c500,
|
|
||||||
},
|
|
||||||
|
|
||||||
modal: {
|
|
||||||
background: tokens.shade.c800,
|
|
||||||
},
|
|
||||||
|
|
||||||
type: {
|
|
||||||
logo: tokens.purple.c100,
|
|
||||||
text: tokens.shade.c50,
|
|
||||||
dimmed: tokens.shade.c50,
|
|
||||||
divider: tokens.ash.c500,
|
|
||||||
secondary: tokens.ash.c100,
|
|
||||||
link: tokens.purple.c100,
|
|
||||||
linkHover: tokens.purple.c50,
|
|
||||||
},
|
|
||||||
|
|
||||||
search: {
|
|
||||||
background: tokens.shade.c500,
|
|
||||||
hoverBackground: tokens.shade.c600,
|
|
||||||
focused: tokens.shade.c400,
|
|
||||||
placeholder: tokens.shade.c100,
|
|
||||||
icon: tokens.shade.c100,
|
|
||||||
},
|
|
||||||
|
|
||||||
mediaCard: {
|
|
||||||
hoverBackground: tokens.shade.c600,
|
|
||||||
hoverAccent: tokens.shade.c50,
|
|
||||||
hoverShadow: tokens.shade.c900,
|
|
||||||
shadow: tokens.shade.c700,
|
|
||||||
barColor: tokens.ash.c200,
|
|
||||||
barFillColor: tokens.purple.c100,
|
|
||||||
badge: tokens.shade.c700,
|
|
||||||
badgeText: tokens.ash.c100,
|
|
||||||
},
|
|
||||||
|
|
||||||
largeCard: {
|
|
||||||
background: tokens.shade.c600,
|
|
||||||
icon: tokens.purple.c400,
|
|
||||||
},
|
|
||||||
|
|
||||||
dropdown: {
|
|
||||||
background: tokens.shade.c600,
|
|
||||||
altBackground: tokens.shade.c700,
|
|
||||||
hoverBackground: tokens.shade.c500,
|
|
||||||
text: tokens.shade.c50,
|
|
||||||
secondary: tokens.shade.c100,
|
|
||||||
border: tokens.shade.c400,
|
|
||||||
contentBackground: tokens.shade.c500,
|
|
||||||
},
|
|
||||||
|
|
||||||
authentication: {
|
|
||||||
border: tokens.shade.c300,
|
|
||||||
inputBg: tokens.shade.c600,
|
|
||||||
inputBgHover: tokens.shade.c500,
|
|
||||||
wordBackground: tokens.shade.c500,
|
|
||||||
copyText: tokens.shade.c100,
|
|
||||||
copyTextHover: tokens.ash.c50,
|
|
||||||
},
|
|
||||||
|
|
||||||
settings: {
|
|
||||||
sidebar: {
|
|
||||||
activeLink: tokens.shade.c600,
|
|
||||||
badge: tokens.shade.c900,
|
|
||||||
|
|
||||||
type: {
|
|
||||||
secondary: tokens.shade.c200,
|
|
||||||
inactive: tokens.shade.c50,
|
|
||||||
icon: tokens.shade.c50,
|
|
||||||
iconActivated: tokens.purple.c200,
|
|
||||||
activated: tokens.purple.c50,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
card: {
|
|
||||||
border: tokens.shade.c400,
|
|
||||||
background: tokens.shade.c400,
|
|
||||||
altBackground: tokens.shade.c400,
|
|
||||||
},
|
|
||||||
|
|
||||||
saveBar: {
|
|
||||||
background: tokens.shade.c800,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
utils: {
|
|
||||||
divider: tokens.ash.c300,
|
|
||||||
},
|
|
||||||
|
|
||||||
errors: {
|
|
||||||
card: tokens.shade.c800,
|
|
||||||
border: tokens.ash.c500,
|
|
||||||
|
|
||||||
type: {
|
|
||||||
secondary: tokens.ash.c100,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
about: {
|
|
||||||
circle: tokens.ash.c500,
|
|
||||||
circleText: tokens.ash.c50,
|
|
||||||
},
|
|
||||||
|
|
||||||
editBadge: {
|
|
||||||
bg: tokens.ash.c500,
|
|
||||||
bgHover: tokens.ash.c400,
|
|
||||||
text: tokens.ash.c50,
|
|
||||||
},
|
|
||||||
|
|
||||||
progress: {
|
|
||||||
background: tokens.ash.c50,
|
|
||||||
preloaded: tokens.ash.c50,
|
|
||||||
filled: tokens.purple.c200,
|
|
||||||
},
|
|
||||||
|
|
||||||
video: {
|
|
||||||
buttonBackground: tokens.ash.c200,
|
|
||||||
|
|
||||||
autoPlay: {
|
|
||||||
background: tokens.ash.c700,
|
|
||||||
hover: tokens.ash.c500,
|
|
||||||
},
|
|
||||||
|
|
||||||
scraping: {
|
|
||||||
card: tokens.shade.c700,
|
|
||||||
loading: tokens.purple.c200,
|
|
||||||
noresult: tokens.ash.c100,
|
|
||||||
},
|
|
||||||
|
|
||||||
audio: {
|
|
||||||
set: tokens.purple.c200,
|
|
||||||
},
|
|
||||||
|
|
||||||
context: {
|
|
||||||
background: tokens.ash.c900,
|
|
||||||
light: tokens.shade.c50,
|
|
||||||
border: tokens.ash.c600,
|
|
||||||
hoverColor: tokens.ash.c600,
|
|
||||||
buttonFocus: tokens.ash.c500,
|
|
||||||
flagBg: tokens.ash.c500,
|
|
||||||
inputBg: tokens.ash.c600,
|
|
||||||
buttonOverInputHover: tokens.ash.c500,
|
|
||||||
inputPlaceholder: tokens.ash.c200,
|
|
||||||
cardBorder: tokens.ash.c700,
|
|
||||||
slider: tokens.ash.c50,
|
|
||||||
sliderFilled: tokens.purple.c200,
|
|
||||||
|
|
||||||
buttons: {
|
|
||||||
list: tokens.ash.c700,
|
|
||||||
active: tokens.ash.c900,
|
|
||||||
},
|
|
||||||
|
|
||||||
closeHover: tokens.ash.c800,
|
|
||||||
|
|
||||||
type: {
|
|
||||||
secondary: tokens.ash.c200,
|
|
||||||
accent: tokens.purple.c200,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
@@ -1,259 +0,0 @@
|
|||||||
import { createTheme } from "../types";
|
|
||||||
|
|
||||||
const tokens = {
|
|
||||||
purple: {
|
|
||||||
c50: "#aaafff",
|
|
||||||
c100: "#8288fe",
|
|
||||||
c200: "#5a62eb",
|
|
||||||
c300: "#454cd4",
|
|
||||||
c400: "#333abe",
|
|
||||||
c500: "#292d86",
|
|
||||||
c600: "#1f2363",
|
|
||||||
c700: "#191b4a",
|
|
||||||
c800: "#111334",
|
|
||||||
c900: "#0b0d22",
|
|
||||||
},
|
|
||||||
shade: {
|
|
||||||
c50: "#7c7c7c",
|
|
||||||
c100: "#666666",
|
|
||||||
c200: "#4f4f4f",
|
|
||||||
c300: "#404040",
|
|
||||||
c400: "#343434",
|
|
||||||
c500: "#282828",
|
|
||||||
c600: "#202020",
|
|
||||||
c700: "#1a1a1a",
|
|
||||||
c800: "#151515",
|
|
||||||
c900: "#0e0e0e",
|
|
||||||
},
|
|
||||||
ash: {
|
|
||||||
c50: "#8d8d8d",
|
|
||||||
c100: "#6b6b6b",
|
|
||||||
c200: "#545454",
|
|
||||||
c300: "#3c3c3c",
|
|
||||||
c400: "#313131",
|
|
||||||
c500: "#2c2c2c",
|
|
||||||
c600: "#252525",
|
|
||||||
c700: "#1e1e1e",
|
|
||||||
c800: "#181818",
|
|
||||||
c900: "#111111",
|
|
||||||
},
|
|
||||||
blue: {
|
|
||||||
c50: "#ccccd6",
|
|
||||||
c100: "#a2a2a2",
|
|
||||||
c200: "#868686",
|
|
||||||
c300: "#646464",
|
|
||||||
c400: "#4e4e4e",
|
|
||||||
c500: "#383838",
|
|
||||||
c600: "#2e2e2e",
|
|
||||||
c700: "#272727",
|
|
||||||
c800: "#181818",
|
|
||||||
c900: "#0f0f0f",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default createTheme({
|
|
||||||
name: "gray",
|
|
||||||
extend: {
|
|
||||||
colors: {
|
|
||||||
themePreview: {
|
|
||||||
primary: tokens.blue.c200,
|
|
||||||
secondary: tokens.shade.c50,
|
|
||||||
},
|
|
||||||
|
|
||||||
pill: {
|
|
||||||
background: tokens.shade.c300,
|
|
||||||
backgroundHover: tokens.shade.c200,
|
|
||||||
highlight: tokens.blue.c200,
|
|
||||||
|
|
||||||
activeBackground: tokens.shade.c300,
|
|
||||||
},
|
|
||||||
|
|
||||||
global: {
|
|
||||||
accentA: tokens.blue.c200,
|
|
||||||
accentB: tokens.blue.c300,
|
|
||||||
},
|
|
||||||
|
|
||||||
lightBar: {
|
|
||||||
light: tokens.blue.c400,
|
|
||||||
},
|
|
||||||
|
|
||||||
buttons: {
|
|
||||||
toggle: tokens.purple.c300,
|
|
||||||
toggleDisabled: tokens.ash.c500,
|
|
||||||
|
|
||||||
secondary: tokens.ash.c700,
|
|
||||||
secondaryHover: tokens.ash.c700,
|
|
||||||
purple: tokens.purple.c500,
|
|
||||||
purpleHover: tokens.purple.c400,
|
|
||||||
cancel: tokens.ash.c500,
|
|
||||||
cancelHover: tokens.ash.c300,
|
|
||||||
},
|
|
||||||
|
|
||||||
background: {
|
|
||||||
main: tokens.shade.c900,
|
|
||||||
secondary: tokens.shade.c600,
|
|
||||||
secondaryHover: tokens.shade.c400,
|
|
||||||
accentA: tokens.purple.c500,
|
|
||||||
accentB: tokens.blue.c500,
|
|
||||||
},
|
|
||||||
|
|
||||||
modal: {
|
|
||||||
background: tokens.shade.c800,
|
|
||||||
},
|
|
||||||
|
|
||||||
type: {
|
|
||||||
logo: tokens.purple.c100,
|
|
||||||
text: tokens.shade.c50,
|
|
||||||
dimmed: tokens.shade.c50,
|
|
||||||
divider: tokens.ash.c500,
|
|
||||||
secondary: tokens.ash.c100,
|
|
||||||
link: tokens.purple.c100,
|
|
||||||
linkHover: tokens.purple.c50,
|
|
||||||
},
|
|
||||||
|
|
||||||
search: {
|
|
||||||
background: tokens.shade.c500,
|
|
||||||
hoverBackground: tokens.shade.c600,
|
|
||||||
focused: tokens.shade.c400,
|
|
||||||
placeholder: tokens.shade.c100,
|
|
||||||
icon: tokens.shade.c100,
|
|
||||||
},
|
|
||||||
|
|
||||||
mediaCard: {
|
|
||||||
hoverBackground: tokens.shade.c600,
|
|
||||||
hoverAccent: tokens.shade.c50,
|
|
||||||
hoverShadow: tokens.shade.c900,
|
|
||||||
shadow: tokens.shade.c700,
|
|
||||||
barColor: tokens.ash.c200,
|
|
||||||
barFillColor: tokens.purple.c100,
|
|
||||||
badge: tokens.shade.c700,
|
|
||||||
badgeText: tokens.ash.c100,
|
|
||||||
},
|
|
||||||
|
|
||||||
largeCard: {
|
|
||||||
background: tokens.shade.c600,
|
|
||||||
icon: tokens.purple.c400,
|
|
||||||
},
|
|
||||||
|
|
||||||
dropdown: {
|
|
||||||
background: tokens.shade.c600,
|
|
||||||
altBackground: tokens.shade.c700,
|
|
||||||
hoverBackground: tokens.shade.c500,
|
|
||||||
text: tokens.shade.c50,
|
|
||||||
secondary: tokens.shade.c100,
|
|
||||||
border: tokens.shade.c400,
|
|
||||||
contentBackground: tokens.shade.c500,
|
|
||||||
},
|
|
||||||
|
|
||||||
authentication: {
|
|
||||||
border: tokens.shade.c300,
|
|
||||||
inputBg: tokens.shade.c600,
|
|
||||||
inputBgHover: tokens.shade.c500,
|
|
||||||
wordBackground: tokens.shade.c500,
|
|
||||||
copyText: tokens.shade.c100,
|
|
||||||
copyTextHover: tokens.ash.c50,
|
|
||||||
},
|
|
||||||
|
|
||||||
settings: {
|
|
||||||
sidebar: {
|
|
||||||
activeLink: tokens.shade.c600,
|
|
||||||
badge: tokens.shade.c900,
|
|
||||||
|
|
||||||
type: {
|
|
||||||
secondary: tokens.shade.c200,
|
|
||||||
inactive: tokens.shade.c50,
|
|
||||||
icon: tokens.shade.c50,
|
|
||||||
iconActivated: tokens.purple.c200,
|
|
||||||
activated: tokens.purple.c50,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
card: {
|
|
||||||
border: tokens.shade.c400,
|
|
||||||
background: tokens.shade.c400,
|
|
||||||
altBackground: tokens.shade.c400,
|
|
||||||
},
|
|
||||||
|
|
||||||
saveBar: {
|
|
||||||
background: tokens.shade.c800,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
utils: {
|
|
||||||
divider: tokens.ash.c300,
|
|
||||||
},
|
|
||||||
|
|
||||||
errors: {
|
|
||||||
card: tokens.shade.c800,
|
|
||||||
border: tokens.ash.c500,
|
|
||||||
|
|
||||||
type: {
|
|
||||||
secondary: tokens.ash.c100,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
about: {
|
|
||||||
circle: tokens.ash.c500,
|
|
||||||
circleText: tokens.ash.c50,
|
|
||||||
},
|
|
||||||
|
|
||||||
editBadge: {
|
|
||||||
bg: tokens.ash.c500,
|
|
||||||
bgHover: tokens.ash.c400,
|
|
||||||
text: tokens.ash.c50,
|
|
||||||
},
|
|
||||||
|
|
||||||
progress: {
|
|
||||||
background: tokens.ash.c50,
|
|
||||||
preloaded: tokens.ash.c50,
|
|
||||||
filled: tokens.purple.c200,
|
|
||||||
},
|
|
||||||
|
|
||||||
video: {
|
|
||||||
buttonBackground: tokens.ash.c200,
|
|
||||||
|
|
||||||
autoPlay: {
|
|
||||||
background: tokens.ash.c700,
|
|
||||||
hover: tokens.ash.c500,
|
|
||||||
},
|
|
||||||
|
|
||||||
scraping: {
|
|
||||||
card: tokens.shade.c700,
|
|
||||||
loading: tokens.purple.c200,
|
|
||||||
noresult: tokens.ash.c100,
|
|
||||||
},
|
|
||||||
|
|
||||||
audio: {
|
|
||||||
set: tokens.purple.c200,
|
|
||||||
},
|
|
||||||
|
|
||||||
context: {
|
|
||||||
background: tokens.ash.c900,
|
|
||||||
light: tokens.shade.c50,
|
|
||||||
border: tokens.ash.c600,
|
|
||||||
hoverColor: tokens.ash.c600,
|
|
||||||
buttonFocus: tokens.ash.c500,
|
|
||||||
flagBg: tokens.ash.c500,
|
|
||||||
inputBg: tokens.ash.c600,
|
|
||||||
buttonOverInputHover: tokens.ash.c500,
|
|
||||||
inputPlaceholder: tokens.ash.c200,
|
|
||||||
cardBorder: tokens.ash.c700,
|
|
||||||
slider: tokens.ash.c50,
|
|
||||||
sliderFilled: tokens.purple.c200,
|
|
||||||
|
|
||||||
buttons: {
|
|
||||||
list: tokens.ash.c700,
|
|
||||||
active: tokens.ash.c900,
|
|
||||||
},
|
|
||||||
|
|
||||||
closeHover: tokens.ash.c800,
|
|
||||||
|
|
||||||
type: {
|
|
||||||
secondary: tokens.ash.c200,
|
|
||||||
accent: tokens.purple.c200,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
@@ -1,259 +0,0 @@
|
|||||||
import { createTheme } from "../types";
|
|
||||||
|
|
||||||
const tokens = {
|
|
||||||
purple: {
|
|
||||||
c50: "#feabac",
|
|
||||||
c100: "#fe8385",
|
|
||||||
c200: "#ea5b5e",
|
|
||||||
c300: "#d34648",
|
|
||||||
c400: "#bd3436",
|
|
||||||
c500: "#852a2b",
|
|
||||||
c600: "#632021",
|
|
||||||
c700: "#49191a",
|
|
||||||
c800: "#331112",
|
|
||||||
c900: "#220c0c",
|
|
||||||
},
|
|
||||||
shade: {
|
|
||||||
c50: "#9c605c",
|
|
||||||
c100: "#834d49",
|
|
||||||
c200: "#673b38",
|
|
||||||
c300: "#542f2c",
|
|
||||||
c400: "#452422",
|
|
||||||
c500: "#361c1a",
|
|
||||||
c600: "#2b1614",
|
|
||||||
c700: "#241210",
|
|
||||||
c800: "#1c0e0d",
|
|
||||||
c900: "#130909",
|
|
||||||
},
|
|
||||||
ash: {
|
|
||||||
c50: "#ac6e6f",
|
|
||||||
c100: "#8b4b4c",
|
|
||||||
c200: "#703739",
|
|
||||||
c300: "#572225",
|
|
||||||
c400: "#49191a",
|
|
||||||
c500: "#421617",
|
|
||||||
c600: "#371212",
|
|
||||||
c700: "#2e0e0f",
|
|
||||||
c800: "#230c0d",
|
|
||||||
c900: "#19090b",
|
|
||||||
},
|
|
||||||
blue: {
|
|
||||||
c50: "#f5adb4",
|
|
||||||
c100: "#cc7981",
|
|
||||||
c200: "#ae5d65",
|
|
||||||
c300: "#8c3b43",
|
|
||||||
c400: "#712a31",
|
|
||||||
c500: "#501f24",
|
|
||||||
c600: "#411b1f",
|
|
||||||
c700: "#36171b",
|
|
||||||
c800: "#201011",
|
|
||||||
c900: "#130b0c",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default createTheme({
|
|
||||||
name: "red",
|
|
||||||
extend: {
|
|
||||||
colors: {
|
|
||||||
themePreview: {
|
|
||||||
primary: tokens.blue.c200,
|
|
||||||
secondary: tokens.shade.c50,
|
|
||||||
},
|
|
||||||
|
|
||||||
pill: {
|
|
||||||
background: tokens.shade.c300,
|
|
||||||
backgroundHover: tokens.shade.c200,
|
|
||||||
highlight: tokens.blue.c200,
|
|
||||||
|
|
||||||
activeBackground: tokens.shade.c300,
|
|
||||||
},
|
|
||||||
|
|
||||||
global: {
|
|
||||||
accentA: tokens.blue.c200,
|
|
||||||
accentB: tokens.blue.c300,
|
|
||||||
},
|
|
||||||
|
|
||||||
lightBar: {
|
|
||||||
light: tokens.blue.c400,
|
|
||||||
},
|
|
||||||
|
|
||||||
buttons: {
|
|
||||||
toggle: tokens.purple.c300,
|
|
||||||
toggleDisabled: tokens.ash.c500,
|
|
||||||
|
|
||||||
secondary: tokens.ash.c700,
|
|
||||||
secondaryHover: tokens.ash.c700,
|
|
||||||
purple: tokens.purple.c500,
|
|
||||||
purpleHover: tokens.purple.c400,
|
|
||||||
cancel: tokens.ash.c500,
|
|
||||||
cancelHover: tokens.ash.c300,
|
|
||||||
},
|
|
||||||
|
|
||||||
background: {
|
|
||||||
main: tokens.shade.c900,
|
|
||||||
secondary: tokens.shade.c600,
|
|
||||||
secondaryHover: tokens.shade.c400,
|
|
||||||
accentA: tokens.purple.c500,
|
|
||||||
accentB: tokens.blue.c500,
|
|
||||||
},
|
|
||||||
|
|
||||||
modal: {
|
|
||||||
background: tokens.shade.c800,
|
|
||||||
},
|
|
||||||
|
|
||||||
type: {
|
|
||||||
logo: tokens.purple.c100,
|
|
||||||
text: tokens.shade.c50,
|
|
||||||
dimmed: tokens.shade.c50,
|
|
||||||
divider: tokens.ash.c500,
|
|
||||||
secondary: tokens.ash.c100,
|
|
||||||
link: tokens.purple.c100,
|
|
||||||
linkHover: tokens.purple.c50,
|
|
||||||
},
|
|
||||||
|
|
||||||
search: {
|
|
||||||
background: tokens.shade.c500,
|
|
||||||
hoverBackground: tokens.shade.c600,
|
|
||||||
focused: tokens.shade.c400,
|
|
||||||
placeholder: tokens.shade.c100,
|
|
||||||
icon: tokens.shade.c100,
|
|
||||||
},
|
|
||||||
|
|
||||||
mediaCard: {
|
|
||||||
hoverBackground: tokens.shade.c600,
|
|
||||||
hoverAccent: tokens.shade.c50,
|
|
||||||
hoverShadow: tokens.shade.c900,
|
|
||||||
shadow: tokens.shade.c700,
|
|
||||||
barColor: tokens.ash.c200,
|
|
||||||
barFillColor: tokens.purple.c100,
|
|
||||||
badge: tokens.shade.c700,
|
|
||||||
badgeText: tokens.ash.c100,
|
|
||||||
},
|
|
||||||
|
|
||||||
largeCard: {
|
|
||||||
background: tokens.shade.c600,
|
|
||||||
icon: tokens.purple.c400,
|
|
||||||
},
|
|
||||||
|
|
||||||
dropdown: {
|
|
||||||
background: tokens.shade.c600,
|
|
||||||
altBackground: tokens.shade.c700,
|
|
||||||
hoverBackground: tokens.shade.c500,
|
|
||||||
text: tokens.shade.c50,
|
|
||||||
secondary: tokens.shade.c100,
|
|
||||||
border: tokens.shade.c400,
|
|
||||||
contentBackground: tokens.shade.c500,
|
|
||||||
},
|
|
||||||
|
|
||||||
authentication: {
|
|
||||||
border: tokens.shade.c300,
|
|
||||||
inputBg: tokens.shade.c600,
|
|
||||||
inputBgHover: tokens.shade.c500,
|
|
||||||
wordBackground: tokens.shade.c500,
|
|
||||||
copyText: tokens.shade.c100,
|
|
||||||
copyTextHover: tokens.ash.c50,
|
|
||||||
},
|
|
||||||
|
|
||||||
settings: {
|
|
||||||
sidebar: {
|
|
||||||
activeLink: tokens.shade.c600,
|
|
||||||
badge: tokens.shade.c900,
|
|
||||||
|
|
||||||
type: {
|
|
||||||
secondary: tokens.shade.c200,
|
|
||||||
inactive: tokens.shade.c50,
|
|
||||||
icon: tokens.shade.c50,
|
|
||||||
iconActivated: tokens.purple.c200,
|
|
||||||
activated: tokens.purple.c50,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
card: {
|
|
||||||
border: tokens.shade.c400,
|
|
||||||
background: tokens.shade.c400,
|
|
||||||
altBackground: tokens.shade.c400,
|
|
||||||
},
|
|
||||||
|
|
||||||
saveBar: {
|
|
||||||
background: tokens.shade.c800,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
utils: {
|
|
||||||
divider: tokens.ash.c300,
|
|
||||||
},
|
|
||||||
|
|
||||||
errors: {
|
|
||||||
card: tokens.shade.c800,
|
|
||||||
border: tokens.ash.c500,
|
|
||||||
|
|
||||||
type: {
|
|
||||||
secondary: tokens.ash.c100,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
about: {
|
|
||||||
circle: tokens.ash.c500,
|
|
||||||
circleText: tokens.ash.c50,
|
|
||||||
},
|
|
||||||
|
|
||||||
editBadge: {
|
|
||||||
bg: tokens.ash.c500,
|
|
||||||
bgHover: tokens.ash.c400,
|
|
||||||
text: tokens.ash.c50,
|
|
||||||
},
|
|
||||||
|
|
||||||
progress: {
|
|
||||||
background: tokens.ash.c50,
|
|
||||||
preloaded: tokens.ash.c50,
|
|
||||||
filled: tokens.purple.c200,
|
|
||||||
},
|
|
||||||
|
|
||||||
video: {
|
|
||||||
buttonBackground: tokens.ash.c200,
|
|
||||||
|
|
||||||
autoPlay: {
|
|
||||||
background: tokens.ash.c700,
|
|
||||||
hover: tokens.ash.c500,
|
|
||||||
},
|
|
||||||
|
|
||||||
scraping: {
|
|
||||||
card: tokens.shade.c700,
|
|
||||||
loading: tokens.purple.c200,
|
|
||||||
noresult: tokens.ash.c100,
|
|
||||||
},
|
|
||||||
|
|
||||||
audio: {
|
|
||||||
set: tokens.purple.c200,
|
|
||||||
},
|
|
||||||
|
|
||||||
context: {
|
|
||||||
background: tokens.ash.c900,
|
|
||||||
light: tokens.shade.c50,
|
|
||||||
border: tokens.ash.c600,
|
|
||||||
hoverColor: tokens.ash.c600,
|
|
||||||
buttonFocus: tokens.ash.c500,
|
|
||||||
flagBg: tokens.ash.c500,
|
|
||||||
inputBg: tokens.ash.c600,
|
|
||||||
buttonOverInputHover: tokens.ash.c500,
|
|
||||||
inputPlaceholder: tokens.ash.c200,
|
|
||||||
cardBorder: tokens.ash.c700,
|
|
||||||
slider: tokens.ash.c50,
|
|
||||||
sliderFilled: tokens.purple.c200,
|
|
||||||
|
|
||||||
buttons: {
|
|
||||||
list: tokens.ash.c700,
|
|
||||||
active: tokens.ash.c900,
|
|
||||||
},
|
|
||||||
|
|
||||||
closeHover: tokens.ash.c800,
|
|
||||||
|
|
||||||
type: {
|
|
||||||
secondary: tokens.ash.c200,
|
|
||||||
accent: tokens.purple.c200,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
@@ -1,259 +0,0 @@
|
|||||||
import { createTheme } from "../types";
|
|
||||||
|
|
||||||
const tokens = {
|
|
||||||
purple: {
|
|
||||||
c50: "#aad7ff",
|
|
||||||
c100: "#82c4ff",
|
|
||||||
c200: "#59a8ec",
|
|
||||||
c300: "#4491d6",
|
|
||||||
c400: "#317dbf",
|
|
||||||
c500: "#285b87",
|
|
||||||
c600: "#1f4464",
|
|
||||||
c700: "#18334a",
|
|
||||||
c800: "#112434",
|
|
||||||
c900: "#0b1822",
|
|
||||||
},
|
|
||||||
shade: {
|
|
||||||
c50: "#677c90",
|
|
||||||
c100: "#52667a",
|
|
||||||
c200: "#3f4f60",
|
|
||||||
c300: "#32404f",
|
|
||||||
c400: "#273441",
|
|
||||||
c500: "#1e2832",
|
|
||||||
c600: "#172028",
|
|
||||||
c700: "#131a22",
|
|
||||||
c800: "#0f151b",
|
|
||||||
c900: "#0a0e12",
|
|
||||||
},
|
|
||||||
ash: {
|
|
||||||
c50: "#7f9b9b",
|
|
||||||
c100: "#5b7b7b",
|
|
||||||
c200: "#446463",
|
|
||||||
c300: "#2b4e4d",
|
|
||||||
c400: "#204241",
|
|
||||||
c500: "#1c3c3b",
|
|
||||||
c600: "#173232",
|
|
||||||
c700: "#132929",
|
|
||||||
c800: "#102020",
|
|
||||||
c900: "#0c1615",
|
|
||||||
},
|
|
||||||
blue: {
|
|
||||||
c50: "#adf5d6",
|
|
||||||
c100: "#79cca8",
|
|
||||||
c200: "#5dae8b",
|
|
||||||
c300: "#3b8c69",
|
|
||||||
c400: "#2a7152",
|
|
||||||
c500: "#1f503b",
|
|
||||||
c600: "#1b4130",
|
|
||||||
c700: "#173629",
|
|
||||||
c800: "#102019",
|
|
||||||
c900: "#0b1310",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default createTheme({
|
|
||||||
name: "teal",
|
|
||||||
extend: {
|
|
||||||
colors: {
|
|
||||||
themePreview: {
|
|
||||||
primary: tokens.blue.c200,
|
|
||||||
secondary: tokens.shade.c50,
|
|
||||||
},
|
|
||||||
|
|
||||||
pill: {
|
|
||||||
background: tokens.shade.c300,
|
|
||||||
backgroundHover: tokens.shade.c200,
|
|
||||||
highlight: tokens.blue.c200,
|
|
||||||
|
|
||||||
activeBackground: tokens.shade.c300,
|
|
||||||
},
|
|
||||||
|
|
||||||
global: {
|
|
||||||
accentA: tokens.blue.c200,
|
|
||||||
accentB: tokens.blue.c300,
|
|
||||||
},
|
|
||||||
|
|
||||||
lightBar: {
|
|
||||||
light: tokens.blue.c400,
|
|
||||||
},
|
|
||||||
|
|
||||||
buttons: {
|
|
||||||
toggle: tokens.purple.c300,
|
|
||||||
toggleDisabled: tokens.ash.c500,
|
|
||||||
|
|
||||||
secondary: tokens.ash.c700,
|
|
||||||
secondaryHover: tokens.ash.c700,
|
|
||||||
purple: tokens.purple.c500,
|
|
||||||
purpleHover: tokens.purple.c400,
|
|
||||||
cancel: tokens.ash.c500,
|
|
||||||
cancelHover: tokens.ash.c300,
|
|
||||||
},
|
|
||||||
|
|
||||||
background: {
|
|
||||||
main: tokens.shade.c900,
|
|
||||||
secondary: tokens.shade.c600,
|
|
||||||
secondaryHover: tokens.shade.c400,
|
|
||||||
accentA: tokens.purple.c500,
|
|
||||||
accentB: tokens.blue.c500,
|
|
||||||
},
|
|
||||||
|
|
||||||
modal: {
|
|
||||||
background: tokens.shade.c800,
|
|
||||||
},
|
|
||||||
|
|
||||||
type: {
|
|
||||||
logo: tokens.purple.c100,
|
|
||||||
text: tokens.shade.c50,
|
|
||||||
dimmed: tokens.shade.c50,
|
|
||||||
divider: tokens.ash.c500,
|
|
||||||
secondary: tokens.ash.c100,
|
|
||||||
link: tokens.purple.c100,
|
|
||||||
linkHover: tokens.purple.c50,
|
|
||||||
},
|
|
||||||
|
|
||||||
search: {
|
|
||||||
background: tokens.shade.c500,
|
|
||||||
hoverBackground: tokens.shade.c600,
|
|
||||||
focused: tokens.shade.c400,
|
|
||||||
placeholder: tokens.shade.c100,
|
|
||||||
icon: tokens.shade.c100,
|
|
||||||
},
|
|
||||||
|
|
||||||
mediaCard: {
|
|
||||||
hoverBackground: tokens.shade.c600,
|
|
||||||
hoverAccent: tokens.shade.c50,
|
|
||||||
hoverShadow: tokens.shade.c900,
|
|
||||||
shadow: tokens.shade.c700,
|
|
||||||
barColor: tokens.ash.c200,
|
|
||||||
barFillColor: tokens.purple.c100,
|
|
||||||
badge: tokens.shade.c700,
|
|
||||||
badgeText: tokens.ash.c100,
|
|
||||||
},
|
|
||||||
|
|
||||||
largeCard: {
|
|
||||||
background: tokens.shade.c600,
|
|
||||||
icon: tokens.purple.c400,
|
|
||||||
},
|
|
||||||
|
|
||||||
dropdown: {
|
|
||||||
background: tokens.shade.c600,
|
|
||||||
altBackground: tokens.shade.c700,
|
|
||||||
hoverBackground: tokens.shade.c500,
|
|
||||||
text: tokens.shade.c50,
|
|
||||||
secondary: tokens.shade.c100,
|
|
||||||
border: tokens.shade.c400,
|
|
||||||
contentBackground: tokens.shade.c500,
|
|
||||||
},
|
|
||||||
|
|
||||||
authentication: {
|
|
||||||
border: tokens.shade.c300,
|
|
||||||
inputBg: tokens.shade.c600,
|
|
||||||
inputBgHover: tokens.shade.c500,
|
|
||||||
wordBackground: tokens.shade.c500,
|
|
||||||
copyText: tokens.shade.c100,
|
|
||||||
copyTextHover: tokens.ash.c50,
|
|
||||||
},
|
|
||||||
|
|
||||||
settings: {
|
|
||||||
sidebar: {
|
|
||||||
activeLink: tokens.shade.c600,
|
|
||||||
badge: tokens.shade.c900,
|
|
||||||
|
|
||||||
type: {
|
|
||||||
secondary: tokens.shade.c200,
|
|
||||||
inactive: tokens.shade.c50,
|
|
||||||
icon: tokens.shade.c50,
|
|
||||||
iconActivated: tokens.purple.c200,
|
|
||||||
activated: tokens.purple.c50,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
card: {
|
|
||||||
border: tokens.shade.c400,
|
|
||||||
background: tokens.shade.c400,
|
|
||||||
altBackground: tokens.shade.c400,
|
|
||||||
},
|
|
||||||
|
|
||||||
saveBar: {
|
|
||||||
background: tokens.shade.c800,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
utils: {
|
|
||||||
divider: tokens.ash.c300,
|
|
||||||
},
|
|
||||||
|
|
||||||
errors: {
|
|
||||||
card: tokens.shade.c800,
|
|
||||||
border: tokens.ash.c500,
|
|
||||||
|
|
||||||
type: {
|
|
||||||
secondary: tokens.ash.c100,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
about: {
|
|
||||||
circle: tokens.ash.c500,
|
|
||||||
circleText: tokens.ash.c50,
|
|
||||||
},
|
|
||||||
|
|
||||||
editBadge: {
|
|
||||||
bg: tokens.ash.c500,
|
|
||||||
bgHover: tokens.ash.c400,
|
|
||||||
text: tokens.ash.c50,
|
|
||||||
},
|
|
||||||
|
|
||||||
progress: {
|
|
||||||
background: tokens.ash.c50,
|
|
||||||
preloaded: tokens.ash.c50,
|
|
||||||
filled: tokens.purple.c200,
|
|
||||||
},
|
|
||||||
|
|
||||||
video: {
|
|
||||||
buttonBackground: tokens.ash.c200,
|
|
||||||
|
|
||||||
autoPlay: {
|
|
||||||
background: tokens.ash.c700,
|
|
||||||
hover: tokens.ash.c500,
|
|
||||||
},
|
|
||||||
|
|
||||||
scraping: {
|
|
||||||
card: tokens.shade.c700,
|
|
||||||
loading: tokens.purple.c200,
|
|
||||||
noresult: tokens.ash.c100,
|
|
||||||
},
|
|
||||||
|
|
||||||
audio: {
|
|
||||||
set: tokens.purple.c200,
|
|
||||||
},
|
|
||||||
|
|
||||||
context: {
|
|
||||||
background: tokens.ash.c900,
|
|
||||||
light: tokens.shade.c50,
|
|
||||||
border: tokens.ash.c600,
|
|
||||||
hoverColor: tokens.ash.c600,
|
|
||||||
buttonFocus: tokens.ash.c500,
|
|
||||||
flagBg: tokens.ash.c500,
|
|
||||||
inputBg: tokens.ash.c600,
|
|
||||||
buttonOverInputHover: tokens.ash.c500,
|
|
||||||
inputPlaceholder: tokens.ash.c200,
|
|
||||||
cardBorder: tokens.ash.c700,
|
|
||||||
slider: tokens.ash.c50,
|
|
||||||
sliderFilled: tokens.purple.c200,
|
|
||||||
|
|
||||||
buttons: {
|
|
||||||
list: tokens.ash.c700,
|
|
||||||
active: tokens.ash.c900,
|
|
||||||
},
|
|
||||||
|
|
||||||
closeHover: tokens.ash.c800,
|
|
||||||
|
|
||||||
type: {
|
|
||||||
secondary: tokens.ash.c200,
|
|
||||||
accent: tokens.purple.c200,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
@@ -1,18 +0,0 @@
|
|||||||
import type { defaultTheme } from "./default";
|
|
||||||
|
|
||||||
type DeepPartial<T> = {
|
|
||||||
[P in keyof T]?: DeepPartial<T[P]>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface Theme {
|
|
||||||
name: string;
|
|
||||||
extend: DeepPartial<(typeof defaultTheme)["extend"]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createTheme(theme: Theme) {
|
|
||||||
return {
|
|
||||||
name: theme.name,
|
|
||||||
selectors: [`.theme-${theme.name}`],
|
|
||||||
extend: theme.extend,
|
|
||||||
};
|
|
||||||
}
|
|
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "@movie-web/tsconfig/base.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json",
|
|
||||||
},
|
|
||||||
"include": ["."],
|
|
||||||
"exclude": ["node_modules"],
|
|
||||||
}
|
|
Reference in New Issue
Block a user