diff --git a/OwnTube.tv/app/(home)/index.tsx b/OwnTube.tv/app/(home)/index.tsx index 85ada03..59f4b6b 100644 --- a/OwnTube.tv/app/(home)/index.tsx +++ b/OwnTube.tv/app/(home)/index.tsx @@ -8,13 +8,16 @@ import { HomeScreen } from "../../screens"; import { Feather } from "@expo/vector-icons"; import { useTheme } from "@react-navigation/native"; import { StyleSheet, View } from "react-native"; +import { useRecentInstances } from "../../hooks"; +import { RootStackParams } from "../_layout"; export default function index() { const router = useRouter(); const navigation = useNavigation(); const theme = useTheme(); - const { backend } = useLocalSearchParams(); + const { backend } = useLocalSearchParams(); const [isGettingStoredBackend, setIsGettingStoredBackend] = useState(true); + const { recentInstances, addRecentInstance } = useRecentInstances(); const getSourceAndRedirect = async () => { if (backend) { @@ -39,17 +42,24 @@ export default function index() { title: `OwnTube.tv@${backend}`, headerRight: () => ( - + ), }); + + if (!(recentInstances?.[0] === backend)) { + addRecentInstance(backend); + } } getSourceAndRedirect(); - }, [backend]), + }, [backend, recentInstances]), ); if (isGettingStoredBackend) { diff --git a/OwnTube.tv/app/_layout.tsx b/OwnTube.tv/app/_layout.tsx index d71900a..8155950 100644 --- a/OwnTube.tv/app/_layout.tsx +++ b/OwnTube.tv/app/_layout.tsx @@ -79,7 +79,7 @@ export const unstable_settings = { export type RootStackParams = { [ROUTES.INDEX]: { backend: string }; - [ROUTES.SETTINGS]: { backend: string }; + [ROUTES.SETTINGS]: { backend: string; tab: "history" | "instance" | "config" }; [ROUTES.VIDEO]: { backend: string; id: string; timestamp?: string }; }; diff --git a/OwnTube.tv/components/ComboBoxInput.tsx b/OwnTube.tv/components/ComboBoxInput.tsx index cfbef23..6ee7a79 100644 --- a/OwnTube.tv/components/ComboBoxInput.tsx +++ b/OwnTube.tv/components/ComboBoxInput.tsx @@ -80,7 +80,7 @@ export const ComboBoxInput = ({ value = "", onChange, data = [], testID }: Combo ); return ( - + {isDropDownVisible && ( - + ({ ], }), })); +jest.mock("../hooks", () => ({ + ...jest.requireActual("../hooks"), + useRecentInstances: jest.fn(() => ({ + recentInstances: [], + addRecentInstance: jest.fn(), + clearRecentInstances: jest.fn(), + })), +})); jest.mock("./ComboBoxInput", () => ({ ComboBoxInput: "ComboBoxInput", diff --git a/OwnTube.tv/components/SourceSelect.tsx b/OwnTube.tv/components/SourceSelect.tsx index 3372c4c..35d62b8 100644 --- a/OwnTube.tv/components/SourceSelect.tsx +++ b/OwnTube.tv/components/SourceSelect.tsx @@ -10,15 +10,19 @@ import { useGetInstancesQuery } from "../api"; import { ComboBoxInput } from "./ComboBoxInput"; import { Spacer } from "./shared/Spacer"; import { colors } from "../colors"; +import { useRecentInstances } from "../hooks"; +import { Ionicons } from "@expo/vector-icons"; export const SourceSelect = () => { const { backend } = useLocalSearchParams(); const router = useRouter(); const theme = useTheme(); + const { recentInstances, addRecentInstance, clearRecentInstances } = useRecentInstances(); const handleSelectSource = (backend: string) => { router.setParams({ backend }); writeToAsyncStorage(STORAGE.DATASOURCE, backend); + addRecentInstance(backend); }; const renderItem = useCallback( @@ -57,6 +61,23 @@ export const SourceSelect = () => { )} + {!!recentInstances?.length && ( + <> + + Recent instances: + + Clear + + + {recentInstances?.map(renderItem)} + + )} + Predefined instances: {Object.values(SOURCES).map(renderItem)} {backend && backend in SOURCES && renderItem(backend)} @@ -82,6 +103,8 @@ const styles = StyleSheet.create({ container: { marginTop: 8, }, + iconButton: { borderWidth: 1 }, + recentsHeader: { alignItems: "center", flexDirection: "row", justifyContent: "space-between" }, source: { opacity: 0.5, padding: 5, diff --git a/OwnTube.tv/hooks/index.ts b/OwnTube.tv/hooks/index.ts index 9088c4e..6efc6ef 100644 --- a/OwnTube.tv/hooks/index.ts +++ b/OwnTube.tv/hooks/index.ts @@ -1,3 +1,4 @@ export * from "./useCategoryScroll"; export * from "./useViewHistory"; export * from "./useDeviceCapabilities"; +export * from "./useRecentInstances"; diff --git a/OwnTube.tv/hooks/useRecentInstances.ts b/OwnTube.tv/hooks/useRecentInstances.ts new file mode 100644 index 0000000..72f5a19 --- /dev/null +++ b/OwnTube.tv/hooks/useRecentInstances.ts @@ -0,0 +1,41 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { readFromAsyncStorage, writeToAsyncStorage } from "../utils"; +import { STORAGE } from "../types"; + +export const useRecentInstances = () => { + const queryClient = useQueryClient(); + const { data: recentInstances } = useQuery({ + queryKey: ["recentInstances"], + queryFn: async () => { + const instances: string[] | undefined = await readFromAsyncStorage(STORAGE.RECENT_INSTANCES); + + return instances || []; + }, + select: (data) => { + return data.slice(0, 50); + }, + }); + + const { mutateAsync: updateRecentInstances } = useMutation({ + mutationFn: async (data: string[]) => { + await writeToAsyncStorage(STORAGE.RECENT_INSTANCES, data); + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ["recentInstances"], + }); + }, + }); + + const addRecentInstance = async (instance: string) => { + const currentInstances = queryClient.getQueryData(["recentInstances"]); + + await updateRecentInstances(Array.from(new Set([instance, ...(currentInstances || [])]))); + }; + + const clearRecentInstances = async () => { + await updateRecentInstances([]); + }; + + return { recentInstances, addRecentInstance, clearRecentInstances }; +}; diff --git a/OwnTube.tv/screens/SettingsScreen/index.tsx b/OwnTube.tv/screens/SettingsScreen/index.tsx index 41c9f9c..d27deef 100644 --- a/OwnTube.tv/screens/SettingsScreen/index.tsx +++ b/OwnTube.tv/screens/SettingsScreen/index.tsx @@ -3,7 +3,10 @@ import { SourceSelect, Typography, ViewHistory, AppConfig } from "../../componen import { Screen } from "../../layouts"; import { styles } from "./styles"; import { useTheme } from "@react-navigation/native"; -import React, { useState } from "react"; +import React from "react"; +import { useLocalSearchParams, useRouter } from "expo-router"; +import { RootStackParams } from "../../app/_layout"; +import { ROUTES } from "../../types"; type SettingsTab = "history" | "instance" | "config"; @@ -15,7 +18,8 @@ const tabsWithNames: Record = { export const SettingsScreen = () => { const { colors } = useTheme(); - const [tab, setTab] = useState("history"); + const { tab } = useLocalSearchParams(); + const router = useRouter(); const tabContent: Record = { history: , @@ -23,6 +27,10 @@ export const SettingsScreen = () => { config: , }; + const setTab = (newTab: SettingsTab) => () => { + router.setParams({ tab: newTab }); + }; + return ( { ]} > {Object.entries(tabsWithNames).map(([key, label]) => ( - setTab(key as SettingsTab)}> + {label} ))} - {tabContent[tab]} + {!!tab && tabContent[tab]} ); }; diff --git a/OwnTube.tv/types.ts b/OwnTube.tv/types.ts index 19434a1..4e36b6b 100644 --- a/OwnTube.tv/types.ts +++ b/OwnTube.tv/types.ts @@ -7,6 +7,7 @@ export enum SOURCES { export enum STORAGE { DATASOURCE = "data_source", VIEW_HISTORY = "view_history", + RECENT_INSTANCES = "recent_instances", } export enum ROUTES {