From b58fa86a6b9aff6aa2448c9646037dd5efeaa6b5 Mon Sep 17 00:00:00 2001 From: herrrta <73949927+herrrta@users.noreply.github.com> Date: Mon, 30 Dec 2024 16:00:04 -0500 Subject: [PATCH 01/19] Better jellyseerr password input with loading indicator --- components/settings/SettingToggles.tsx | 109 ++++++++++++++++--------- 1 file changed, 69 insertions(+), 40 deletions(-) diff --git a/components/settings/SettingToggles.tsx b/components/settings/SettingToggles.tsx index 2f55edaf..73d197a6 100644 --- a/components/settings/SettingToggles.tsx +++ b/components/settings/SettingToggles.tsx @@ -21,9 +21,8 @@ import * as BackgroundFetch from "expo-background-fetch"; import * as ScreenOrientation from "expo-screen-orientation"; import * as TaskManager from "expo-task-manager"; import { useAtom } from "jotai"; -import React, {useCallback, useEffect, useState} from "react"; +import React, {useCallback, useEffect, useRef, useState} from "react"; import { - Alert, Linking, Switch, TouchableOpacity, @@ -59,7 +58,10 @@ export const SettingToggles: React.FC = ({ ...props }) => { const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); + const jellyseerrPassInputRef = useRef(null); const [marlinUrl, setMarlinUrl] = useState(""); + const [promptForJellyseerrPass, setPromptForJellyseerrPass] = useState(false); + const [isJellyseerrLoading, setIsLoadingJellyseerr] = useState(false); const [jellyseerrPassword, setJellyseerrPassword] = useState(undefined); const [optimizedVersionsServerUrl, setOptimizedVersionsServerUrl] = useState(settings?.optimizedVersionsServerUrl || ""); @@ -121,39 +123,9 @@ export const SettingToggles: React.FC = ({ ...props }) => { staleTime: 0, }); - const promptForJellyseerrLogin = useCallback(() => - Alert.prompt( - 'Enter jellyfin password', - `Enter password for jellyfin user ${user?.Name}`, - (input) => setJellyseerrPassword(input), - 'secure-text' - ), - [user, setJellyseerrPassword] - ); - - const testJellyseerrServerUrl = useCallback(async () => { - if (!jellyseerrServerUrl || jellyseerrApi) - return; - - const jellyseerrTempApi = new JellyseerrApi(jellyseerrServerUrl); - - jellyseerrTempApi.test().then(result => { - if (result.isValid) { - - if (result.requiresPass) - promptForJellyseerrLogin() - else - updateSettings({jellyseerrServerUrl}) - } - else { - setjellyseerrServerUrl(undefined); - clearAllJellyseerData(); - } - }) - }, [jellyseerrServerUrl]) - - useEffect(() => { + const loginToJellyseerr = useCallback(() => { if (jellyseerrServerUrl && user?.Name && jellyseerrPassword) { + setIsLoadingJellyseerr(true) const jellyseerrTempApi = new JellyseerrApi(jellyseerrServerUrl); jellyseerrTempApi.login(user?.Name, jellyseerrPassword) .then(user => { @@ -165,10 +137,35 @@ export const SettingToggles: React.FC = ({ ...props }) => { }) .finally(() => { setJellyseerrPassword(undefined); + setPromptForJellyseerrPass(false) + setIsLoadingJellyseerr(false) }) } }, [user, jellyseerrServerUrl, jellyseerrPassword]); + const testJellyseerrServerUrl = useCallback(async () => { + if (!jellyseerrServerUrl || jellyseerrApi) + return; + + setIsLoadingJellyseerr(true) + const jellyseerrTempApi = new JellyseerrApi(jellyseerrServerUrl); + + jellyseerrTempApi.test().then(result => { + if (result.isValid) { + if (result.requiresPass) + setPromptForJellyseerrPass(true) + // promptForJellyseerrLogin() + else + updateSettings({jellyseerrServerUrl}) + } + else { + setPromptForJellyseerrPass(false) + setjellyseerrServerUrl(undefined); + clearAllJellyseerData(); + } + }).finally(() => setIsLoadingJellyseerr(false)) + }, [jellyseerrServerUrl]) + if (!settings) return null; return ( @@ -697,7 +694,7 @@ export const SettingToggles: React.FC = ({ ...props }) => { Jellyseerr - + {jellyseerrUser && <> @@ -718,7 +715,7 @@ export const SettingToggles: React.FC = ({ ...props }) => { : "opacity-50" }`} > - + @@ -741,17 +738,49 @@ export const SettingToggles: React.FC = ({ ...props }) => { textContentType="URL" onChangeText={setjellyseerrServerUrl} /> + + {promptForJellyseerrPass && + + } + {isJellyseerrLoading && + + } From ec0d9d7788f785655aa9edee8c2ff46a02db3194 Mon Sep 17 00:00:00 2001 From: herrrta <73949927+herrrta@users.noreply.github.com> Date: Mon, 30 Dec 2024 16:34:15 -0500 Subject: [PATCH 02/19] Fix spacing between slides on discover page --- app/(auth)/(tabs)/(search)/index.tsx | 2 +- components/jellyseerr/DiscoverSlide.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/(auth)/(tabs)/(search)/index.tsx b/app/(auth)/(tabs)/(search)/index.tsx index aaa5b1ab..979119a6 100644 --- a/app/(auth)/(tabs)/(search)/index.tsx +++ b/app/(auth)/(tabs)/(search)/index.tsx @@ -481,7 +481,7 @@ export default function search() { ))} ) : debouncedSearch.length === 0 && searchType === 'Discover' ? ( - + {sortBy?.(jellyseerrDiscoverSettings?.filter(s => s.enabled), 'order') .map((slide) => ) } diff --git a/components/jellyseerr/DiscoverSlide.tsx b/components/jellyseerr/DiscoverSlide.tsx index 94a2d4dd..ca0f1d6e 100644 --- a/components/jellyseerr/DiscoverSlide.tsx +++ b/components/jellyseerr/DiscoverSlide.tsx @@ -52,7 +52,7 @@ const DiscoverSlide: React.FC = ({slide}) => { return ( (flatData && flatData?.length > 0) && <> - {DiscoverSliderType[slide.type].toString().toTitle()} + {DiscoverSliderType[slide.type].toString().toTitle()} Date: Mon, 30 Dec 2024 16:52:44 -0500 Subject: [PATCH 03/19] Fix movie request crashing --- app/(auth)/(tabs)/(home,libraries,search)/jellyseerr/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/(auth)/(tabs)/(home,libraries,search)/jellyseerr/page.tsx b/app/(auth)/(tabs)/(home,libraries,search)/jellyseerr/page.tsx index bd778042..f537b3f5 100644 --- a/app/(auth)/(tabs)/(home,libraries,search)/jellyseerr/page.tsx +++ b/app/(auth)/(tabs)/(home,libraries,search)/jellyseerr/page.tsx @@ -75,7 +75,7 @@ const Page: React.FC = () => { mediaId: Number(result.id!!), mediaType: result.mediaType!!, tvdbId: details?.externalIds?.tvdbId, - seasons: (details as TvDetails)?.seasons.filter(s => s.seasonNumber !== 0).map(s => s.seasonNumber) + seasons: (details as TvDetails)?.seasons?.filter?.(s => s.seasonNumber !== 0)?.map?.(s => s.seasonNumber) }), [details, result, requestMedia]); return ( From 36a689f59dd2b2a1f71cdd80fdc0b9a7d528dbf1 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Tue, 31 Dec 2024 10:29:50 +0100 Subject: [PATCH 04/19] fix: improved login flow for jellyseerr --- components/Button.tsx | 2 +- components/settings/Jellyseerr.tsx | 207 +++++++++++++++++++++++++ components/settings/SettingToggles.tsx | 156 +------------------ 3 files changed, 213 insertions(+), 152 deletions(-) create mode 100644 components/settings/Jellyseerr.tsx diff --git a/components/Button.tsx b/components/Button.tsx index 1498a975..1a73ad01 100644 --- a/components/Button.tsx +++ b/components/Button.tsx @@ -47,7 +47,7 @@ export const Button: React.FC> = ({ { + const { + jellyseerrApi, + jellyseerrUser, + setJellyseerrUser, + clearAllJellyseerData, + } = useJellyseerr(); + + const [user] = useAtom(userAtom); + const [settings, updateSettings] = useSettings(); + + const [promptForJellyseerrPass, setPromptForJellyseerrPass] = + useState(false); + + const [jellyseerrPassword, setJellyseerrPassword] = useState< + string | undefined + >(undefined); + + const [jellyseerrServerUrl, setjellyseerrServerUrl] = useState< + string | undefined + >(settings?.jellyseerrServerUrl || undefined); + + const loginToJellyseerrMutation = useMutation({ + mutationFn: async () => { + if (!jellyseerrServerUrl || !user?.Name || !jellyseerrPassword) { + throw new Error("Missing required information for login"); + } + const jellyseerrTempApi = new JellyseerrApi(jellyseerrServerUrl); + return jellyseerrTempApi.login(user.Name, jellyseerrPassword); + }, + onSuccess: (user) => { + setJellyseerrUser(user); + updateSettings({ jellyseerrServerUrl }); + }, + onError: () => { + toast.error("Failed to login"); + }, + onSettled: () => { + setJellyseerrPassword(undefined); + }, + }); + + const testJellyseerrServerUrlMutation = useMutation({ + mutationFn: async () => { + if (!jellyseerrServerUrl || jellyseerrApi) return null; + const jellyseerrTempApi = new JellyseerrApi(jellyseerrServerUrl); + return jellyseerrTempApi.test(); + }, + onSuccess: (result) => { + if (result && result.isValid) { + if (result.requiresPass) { + setPromptForJellyseerrPass(true); + } else { + updateSettings({ jellyseerrServerUrl }); + } + } else { + setPromptForJellyseerrPass(false); + setjellyseerrServerUrl(undefined); + clearAllJellyseerData(); + } + }, + }); + + const clearData = () => { + clearAllJellyseerData().finally(() => { + setjellyseerrServerUrl(undefined); + setPromptForJellyseerrPass(false); + }); + }; + + return ( + + Jellyseerr + + {jellyseerrUser ? ( + + + + + + + + + + + ) : ( + + + This integration is in its early stages. Expect things to change. + + Server URL + + + Example: http(s)://your-host.url + + + (add port if required) + + + + + + + + Password + + + + + )} + + + ); +}; diff --git a/components/settings/SettingToggles.tsx b/components/settings/SettingToggles.tsx index 73d197a6..a83f95f8 100644 --- a/components/settings/SettingToggles.tsx +++ b/components/settings/SettingToggles.tsx @@ -21,7 +21,7 @@ import * as BackgroundFetch from "expo-background-fetch"; import * as ScreenOrientation from "expo-screen-orientation"; import * as TaskManager from "expo-task-manager"; import { useAtom } from "jotai"; -import React, {useCallback, useEffect, useRef, useState} from "react"; +import React, { useCallback, useEffect, useRef, useState } from "react"; import { Linking, Switch, @@ -40,35 +40,23 @@ import { Stepper } from "@/components/inputs/Stepper"; import { MediaProvider } from "./MediaContext"; import { SubtitleToggles } from "./SubtitleToggles"; import { AudioToggles } from "./AudioToggles"; -import {JellyseerrApi, useJellyseerr} from "@/hooks/useJellyseerr"; -import {ListItem} from "@/components/ListItem"; +import { JellyseerrApi, useJellyseerr } from "@/hooks/useJellyseerr"; +import { ListItem } from "@/components/ListItem"; +import { JellyseerrSettings } from "./Jellyseerr"; interface Props extends ViewProps {} export const SettingToggles: React.FC = ({ ...props }) => { const [settings, updateSettings] = useSettings(); const { setProcesses } = useDownload(); - const { - jellyseerrApi, - jellyseerrUser, - setJellyseerrUser , - clearAllJellyseerData - } = useJellyseerr(); const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); - const jellyseerrPassInputRef = useRef(null); const [marlinUrl, setMarlinUrl] = useState(""); - const [promptForJellyseerrPass, setPromptForJellyseerrPass] = useState(false); - const [isJellyseerrLoading, setIsLoadingJellyseerr] = useState(false); - const [jellyseerrPassword, setJellyseerrPassword] = useState(undefined); const [optimizedVersionsServerUrl, setOptimizedVersionsServerUrl] = useState(settings?.optimizedVersionsServerUrl || ""); - const [jellyseerrServerUrl, setjellyseerrServerUrl] = - useState(settings?.jellyseerrServerUrl || undefined); - const queryClient = useQueryClient(); /******************** @@ -123,49 +111,6 @@ export const SettingToggles: React.FC = ({ ...props }) => { staleTime: 0, }); - const loginToJellyseerr = useCallback(() => { - if (jellyseerrServerUrl && user?.Name && jellyseerrPassword) { - setIsLoadingJellyseerr(true) - const jellyseerrTempApi = new JellyseerrApi(jellyseerrServerUrl); - jellyseerrTempApi.login(user?.Name, jellyseerrPassword) - .then(user => { - setJellyseerrUser(user); - updateSettings({jellyseerrServerUrl}) - }) - .catch(() => { - toast.error("Failed to login to jellyseerr!") - }) - .finally(() => { - setJellyseerrPassword(undefined); - setPromptForJellyseerrPass(false) - setIsLoadingJellyseerr(false) - }) - } - }, [user, jellyseerrServerUrl, jellyseerrPassword]); - - const testJellyseerrServerUrl = useCallback(async () => { - if (!jellyseerrServerUrl || jellyseerrApi) - return; - - setIsLoadingJellyseerr(true) - const jellyseerrTempApi = new JellyseerrApi(jellyseerrServerUrl); - - jellyseerrTempApi.test().then(result => { - if (result.isValid) { - if (result.requiresPass) - setPromptForJellyseerrPass(true) - // promptForJellyseerrLogin() - else - updateSettings({jellyseerrServerUrl}) - } - else { - setPromptForJellyseerrPass(false) - setjellyseerrServerUrl(undefined); - clearAllJellyseerData(); - } - }).finally(() => setIsLoadingJellyseerr(false)) - }, [jellyseerrServerUrl]) - if (!settings) return null; return ( @@ -692,98 +637,7 @@ export const SettingToggles: React.FC = ({ ...props }) => { - - Jellyseerr - - {jellyseerrUser && <> - - - - - - - - } - - - - - - Server URL - - - - Set the URL for your jellyseerr instance. - - Example: http(s)://your-host.url - (add port if required) - This integration is in its early stages. Expect things to change. - - - - {promptForJellyseerrPass && - - } - - - {isJellyseerrLoading && - - } - - - + ); }; From 5e79b5a5816b0ff594e05e1bc669a125fefa09b9 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Tue, 31 Dec 2024 10:33:59 +0100 Subject: [PATCH 05/19] fix: improve readme --- README.md | 35 +++++------------------------------ 1 file changed, 5 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 360f4949..d5c828a9 100644 --- a/README.md +++ b/README.md @@ -89,36 +89,10 @@ We welcome any help to make Streamyfin better. If you'd like to contribute, plea ### Development info -1. Use node `20` -2. Install dependencies `bun i` -3. Create an expo dev build by running `npx expo run:ios` or `npx expo run:android`. - -## Extended chromecast controls - -Add this to AppDelegate.mm: - -``` -- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions -{ -// @generated begin react-native-google-cast-didFinishLaunchingWithOptions - expo prebuild (DO NOT MODIFY) sync-8901be60b982d2ae9c658b1e8c50634d61bb5091 -#if __has_include() -... - -[GCKCastContext sharedInstance].useDefaultExpandedMediaControls = true;` -#endif -``` - -Add this to Info.plist: - -``` -NSBonjourServices - - _googlecast._tcp - _CC1AD845._googlecast._tcp - -NSLocalNetworkUsageDescription -${PRODUCT_NAME} uses the local network to discover Cast-enabled devices on your WiFi network. -``` +1. Use node `>20` +2. Install dependencies `bun i && bun run submodule-reload` +3. Make sure you have xcode and/or android studio installed. +4. Create an expo dev build by running `npx expo run:ios` or `npx expo run:android`. This will open a simulator on you computer and run the app. ## ๐Ÿ“„ License @@ -153,6 +127,7 @@ I'd like to thank the following people and projects for their contributions to S - [Reiverr](https://github.com/aleksilassila/reiverr) for great help with understanding the Jellyfin API. - [Jellyfin TS SDK](https://github.com/jellyfin/jellyfin-sdk-typescript) for the TypeScript SDK. +- [Jellyseerr](https://github.com/Fallenbagel/jellyseerr) for enabling API integration with their project. - The Jellyfin devs for always being helpful in the Discord. ## Star History From 56f1bd489cba1397cc358593773791c50e186200 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Tue, 31 Dec 2024 10:35:16 +0100 Subject: [PATCH 06/19] fix: improve readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d5c828a9..f63c47f5 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,10 @@ Welcome to Streamyfin, a simple and user-friendly Jellyfin client built with Exp - ๐Ÿš€ **Skp intro / credits support** - ๐Ÿ–ผ๏ธ **Trickplay images**: The new golden standard for chapter previews when seeking. -- ๐Ÿ“บ **Picture in Picture** (iPhone only): Watch movies in PiP mode on your iPhone. - ๐Ÿ”Š **Background audio**: Stream music in the background, even when locking the phone. - ๐Ÿ“ฅ **Download media** (Experimental): Save your media locally and watch it offline. - ๐Ÿ“ก **Chromecast** (Experimental): Cast your media to any Chromecast-enabled device. +- ๐Ÿค– **Jellyseerr integration**: Request media directly in the app. ## ๐Ÿงช Experimental Features From 9174a8104d7f04615680e94f950678a023d9c916 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Tue, 31 Dec 2024 10:55:23 +0100 Subject: [PATCH 07/19] fix: color --- components/ListItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ListItem.tsx b/components/ListItem.tsx index 755f79ed..0287c690 100644 --- a/components/ListItem.tsx +++ b/components/ListItem.tsx @@ -24,7 +24,7 @@ export const ListItem: React.FC> = ({ {title} {subTitle && ( - + {subTitle} )} From 5adf943fd934a6a57ad8c156faedebd4df818732 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Tue, 31 Dec 2024 13:05:23 +0100 Subject: [PATCH 08/19] fix: finally fix not rotten tomatoes score --- assets/images/not-rotten-tomatoes.svg | 65 +++++++++++++++++++++++++++ components/Ratings.tsx | 39 +++++++++------- 2 files changed, 88 insertions(+), 16 deletions(-) create mode 100644 assets/images/not-rotten-tomatoes.svg diff --git a/assets/images/not-rotten-tomatoes.svg b/assets/images/not-rotten-tomatoes.svg new file mode 100644 index 00000000..18fa58b8 --- /dev/null +++ b/assets/images/not-rotten-tomatoes.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/components/Ratings.tsx b/components/Ratings.tsx index e5eb8fc3..f18a0843 100644 --- a/components/Ratings.tsx +++ b/components/Ratings.tsx @@ -3,11 +3,10 @@ import { View, ViewProps } from "react-native"; import { Badge } from "./Badge"; import { Ionicons } from "@expo/vector-icons"; import { Image } from "expo-image"; -import {MovieResult, TvResult} from "@/utils/jellyseerr/server/models/Search"; -import {useJellyseerr} from "@/hooks/useJellyseerr"; -import {useQuery} from "@tanstack/react-query"; -import {MediaType} from "@/utils/jellyseerr/server/constants/media"; - +import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search"; +import { useJellyseerr } from "@/hooks/useJellyseerr"; +import { useQuery } from "@tanstack/react-query"; +import { MediaType } from "@/utils/jellyseerr/server/constants/media"; interface Props extends ViewProps { item?: BaseItemDto | null; } @@ -21,7 +20,7 @@ export const Ratings: React.FC = ({ item, ...props }) => { )} {item.CommunityRating && ( } /> @@ -32,7 +31,11 @@ export const Ratings: React.FC = ({ item, ...props }) => { variant="gray" iconLeft={ = ({ item, ...props }) => { ); }; - -export const JellyserrRatings: React.FC<{result: MovieResult | TvResult}> = ({ result }) => { - const {jellyseerrApi} = useJellyseerr(); +export const JellyserrRatings: React.FC<{ result: MovieResult | TvResult }> = ({ + result, +}) => { + const { jellyseerrApi } = useJellyseerr(); const { data, isLoading } = useQuery({ - queryKey: ['jellyseerr', result.id, result.mediaType, 'ratings'], + queryKey: ["jellyseerr", result.id, result.mediaType, "ratings"], queryFn: async () => { return result.mediaType === MediaType.MOVIE ? jellyseerrApi?.movieRatings(result.id) - : jellyseerrApi?.tvRatings(result.id) + : jellyseerrApi?.tvRatings(result.id); }, staleTime: (5).minutesToMilliseconds(), retry: false, enabled: !!jellyseerrApi, }); - return (isLoading || !!result.voteCount || + return ( + (isLoading || + !!result.voteCount || (data?.criticsRating && !!data?.criticsScore) || (data?.audienceRating && !!data?.audienceScore)) && ( @@ -72,7 +78,7 @@ export const JellyserrRatings: React.FC<{result: MovieResult | TvResult}> = ({ r = ({ r = ({ r )} ) -} \ No newline at end of file + ); +}; From 48422fa93e1274d7448f866201cf574f429e17c6 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Tue, 31 Dec 2024 13:32:43 +0100 Subject: [PATCH 09/19] fix: design --- .../jellyseerr/page.tsx | 226 +++++++++++------- 1 file changed, 137 insertions(+), 89 deletions(-) diff --git a/app/(auth)/(tabs)/(home,libraries,search)/jellyseerr/page.tsx b/app/(auth)/(tabs)/(home,libraries,search)/jellyseerr/page.tsx index f537b3f5..4acd8053 100644 --- a/app/(auth)/(tabs)/(home,libraries,search)/jellyseerr/page.tsx +++ b/app/(auth)/(tabs)/(home,libraries,search)/jellyseerr/page.tsx @@ -1,40 +1,62 @@ -import React, {useCallback, useRef, useState} from "react"; -import {useLocalSearchParams} from "expo-router"; -import {MovieResult, TvResult} from "@/utils/jellyseerr/server/models/Search"; -import {Text} from "@/components/common/Text"; -import {ParallaxScrollView} from "@/components/ParallaxPage"; -import {Image} from "expo-image"; -import {TouchableOpacity, View} from "react-native"; -import {Ionicons} from "@expo/vector-icons"; -import {useSafeAreaInsets} from "react-native-safe-area-context"; -import {OverviewText} from "@/components/OverviewText"; -import {GenreTags} from "@/components/GenreTags"; -import {MediaType} from "@/utils/jellyseerr/server/constants/media"; -import {useQuery} from "@tanstack/react-query"; -import {useJellyseerr} from "@/hooks/useJellyseerr"; -import {Button} from "@/components/Button"; -import {BottomSheetBackdrop, BottomSheetBackdropProps, BottomSheetModal, BottomSheetView} from "@gorhom/bottom-sheet"; -import {IssueType, IssueTypeName} from "@/utils/jellyseerr/server/constants/issue"; +import React, { useCallback, useRef, useState } from "react"; +import { useLocalSearchParams } from "expo-router"; +import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search"; +import { Text } from "@/components/common/Text"; +import { ParallaxScrollView } from "@/components/ParallaxPage"; +import { Image } from "expo-image"; +import { TouchableOpacity, View } from "react-native"; +import { Ionicons } from "@expo/vector-icons"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { OverviewText } from "@/components/OverviewText"; +import { GenreTags } from "@/components/GenreTags"; +import { MediaType } from "@/utils/jellyseerr/server/constants/media"; +import { useQuery } from "@tanstack/react-query"; +import { useJellyseerr } from "@/hooks/useJellyseerr"; +import { Button } from "@/components/Button"; +import { + BottomSheetBackdrop, + BottomSheetBackdropProps, + BottomSheetModal, + BottomSheetView, +} from "@gorhom/bottom-sheet"; +import { + IssueType, + IssueTypeName, +} from "@/utils/jellyseerr/server/constants/issue"; import * as DropdownMenu from "zeego/dropdown-menu"; -import {Input} from "@/components/common/Input"; -import {TvDetails} from "@/utils/jellyseerr/server/models/Tv"; +import { Input } from "@/components/common/Input"; +import { TvDetails } from "@/utils/jellyseerr/server/models/Tv"; import JellyseerrSeasons from "@/components/series/JellyseerrSeasons"; -import {JellyserrRatings} from "@/components/Ratings"; +import { JellyserrRatings } from "@/components/Ratings"; const Page: React.FC = () => { const insets = useSafeAreaInsets(); const params = useLocalSearchParams(); - const {mediaTitle, releaseYear, canRequest: canRequestString, posterSrc, ...result} = - params as unknown as {mediaTitle: string, releaseYear: number, canRequest: string, posterSrc: string} & Partial; + const { + mediaTitle, + releaseYear, + canRequest: canRequestString, + posterSrc, + ...result + } = params as unknown as { + mediaTitle: string; + releaseYear: number; + canRequest: string; + posterSrc: string; + } & Partial; const canRequest = canRequestString === "true"; - const {jellyseerrApi, requestMedia} = useJellyseerr(); + const { jellyseerrApi, requestMedia } = useJellyseerr(); const [issueType, setIssueType] = useState(); const [issueMessage, setIssueMessage] = useState(); const bottomSheetModalRef = useRef(null); - const {data: details, isLoading} = useQuery({ + const { + data: details, + isFetching, + isLoading, + } = useQuery({ enabled: !!jellyseerrApi && !!result && !!result.id, queryKey: ["jellyseerr", "detail", result.mediaType, result.id], staleTime: 0, @@ -45,8 +67,8 @@ const Page: React.FC = () => { queryFn: async () => { return result.mediaType === MediaType.MOVIE ? jellyseerrApi?.movieDetails(result.id!!) - : jellyseerrApi?.tvDetails(result.id!!) - } + : jellyseerrApi?.tvDetails(result.id!!); + }, }); const renderBackdrop = useCallback( @@ -62,23 +84,30 @@ const Page: React.FC = () => { const submitIssue = useCallback(() => { if (result.id && issueType && issueMessage && details) { - jellyseerrApi?.submitIssue(details.mediaInfo.id, Number(issueType), issueMessage) + jellyseerrApi + ?.submitIssue(details.mediaInfo.id, Number(issueType), issueMessage) .then(() => { - setIssueType(undefined) - setIssueMessage(undefined) - bottomSheetModalRef?.current?.close() - }) + setIssueType(undefined); + setIssueMessage(undefined); + bottomSheetModalRef?.current?.close(); + }); } - }, [jellyseerrApi, details, result, issueType, issueMessage]) + }, [jellyseerrApi, details, result, issueType, issueMessage]); - const request = useCallback(() => requestMedia(mediaTitle, { - mediaId: Number(result.id!!), - mediaType: result.mediaType!!, - tvdbId: details?.externalIds?.tvdbId, - seasons: (details as TvDetails)?.seasons?.filter?.(s => s.seasonNumber !== 0)?.map?.(s => s.seasonNumber) - }), [details, result, requestMedia]); + const request = useCallback( + () => + requestMedia(mediaTitle, { + mediaId: Number(result.id!!), + mediaType: result.mediaType!!, + tvdbId: details?.externalIds?.tvdbId, + seasons: (details as TvDetails)?.seasons + ?.filter?.((s) => s.seasonNumber !== 0) + ?.map?.((s) => s.seasonNumber), + }), + [details, result, requestMedia] + ); - return ( + return ( { name="image-outline" size={24} color="white" - style={{opacity: 0.4}} + style={{ opacity: 0.4 }} /> )} @@ -123,12 +152,18 @@ const Page: React.FC = () => { } > - - <> + + - - {mediaTitle} + + + {mediaTitle} + {releaseYear} { }} /> - - g.name) || []} /> - {canRequest ? - - : - - } - + + g.name) || []} /> + + {canRequest ? ( + + ) : ( + + )} + + - {result.mediaType === MediaType.TV && - - } + {result.mediaType === MediaType.TV && ( + + )} @@ -185,17 +225,23 @@ const Page: React.FC = () => { - Whats wrong? + + Whats wrong? + - Issue Type + + Issue Type + - {issueType ? IssueTypeName[issueType] : 'Select an issue' } + {issueType + ? IssueTypeName[issueType] + : "Select an issue"} @@ -210,14 +256,20 @@ const Page: React.FC = () => { sideOffset={0} > Types - {Object.entries(IssueTypeName).reverse().map(([key, value], idx) => ( - setIssueType(key as unknown as IssueType)} - > - {value} - - ))} + {Object.entries(IssueTypeName) + .reverse() + .map(([key, value], idx) => ( + + setIssueType(key as unknown as IssueType) + } + > + + {value} + + + ))} @@ -234,11 +286,7 @@ const Page: React.FC = () => { onChangeText={setIssueMessage} /> - @@ -246,6 +294,6 @@ const Page: React.FC = () => { ); -} +}; -export default Page; \ No newline at end of file +export default Page; From c753e33f38d5264c059bd02ab461d37185c5d7bd Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Tue, 31 Dec 2024 13:32:51 +0100 Subject: [PATCH 10/19] chore --- app/(auth)/(tabs)/(libraries)/[libraryId].tsx | 2 - app/(auth)/player/direct-player.tsx | 7 - components/ItemContent.tsx | 2 - components/series/JellyseerrSeasons.tsx | 352 +++++++++++------- components/settings/MediaContext.tsx | 4 - .../video-player/controls/AudioSlider.tsx | 3 - .../controls/BrightnessSlider.tsx | 1 - components/video-player/controls/Controls.tsx | 5 - .../controls/NextEpisodeCountDownButton.tsx | 1 - .../controls/dropdown/DropdownViewDirect.tsx | 7 - .../dropdown/DropdownViewTranscoding.tsx | 4 - hooks/useIntroSkipper.ts | 1 - hooks/useJellyseerr.ts | 331 +++++++++------- 13 files changed, 403 insertions(+), 317 deletions(-) diff --git a/app/(auth)/(tabs)/(libraries)/[libraryId].tsx b/app/(auth)/(tabs)/(libraries)/[libraryId].tsx index 7d5679a1..bd0df182 100644 --- a/app/(auth)/(tabs)/(libraries)/[libraryId].tsx +++ b/app/(auth)/(tabs)/(libraries)/[libraryId].tsx @@ -141,8 +141,6 @@ const Page = () => { }): Promise => { if (!api || !library) return null; - console.log("[libraryId] ~", library); - let itemType: BaseItemKind | undefined; // This fix makes sure to only return 1 type of items, if defined. diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index 5e62d868..e4b49320 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -282,13 +282,6 @@ export default function page() { if (!item?.Id || !stream) return; - console.log( - "onProgress ~", - currentTimeInTicks, - isPlaying, - `AUDIO index: ${audioIndex} SUB index" ${subtitleIndex}` - ); - await getPlaystateApi(api!).onPlaybackProgress({ itemId: item.Id, audioStreamIndex: audioIndex ? audioIndex : undefined, diff --git a/components/ItemContent.tsx b/components/ItemContent.tsx index 42beba5b..505938eb 100644 --- a/components/ItemContent.tsx +++ b/components/ItemContent.tsx @@ -68,7 +68,6 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( // Needs to automatically change the selected to the default values for default indexes. useEffect(() => { - console.log(defaultAudioIndex, defaultSubtitleIndex); setSelectedOptions(() => ({ bitrate: defaultBitrate, mediaSource: defaultMediaSource, @@ -220,7 +219,6 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo( className="mr-1" source={selectedOptions.mediaSource} onChange={(val) => { - console.log(val); setSelectedOptions( (prev) => prev && { diff --git a/components/series/JellyseerrSeasons.tsx b/components/series/JellyseerrSeasons.tsx index a62689b9..b2a0d2ce 100644 --- a/components/series/JellyseerrSeasons.tsx +++ b/components/series/JellyseerrSeasons.tsx @@ -1,33 +1,37 @@ -import {Text} from "@/components/common/Text"; -import React, {useCallback, useMemo, useState} from "react"; -import {Alert, TouchableOpacity, View} from "react-native"; -import {TvDetails} from "@/utils/jellyseerr/server/models/Tv"; -import {FlashList} from "@shopify/flash-list"; -import {orderBy} from "lodash"; -import {Tags} from "@/components/GenreTags"; +import { Text } from "@/components/common/Text"; +import React, { useCallback, useMemo, useState } from "react"; +import { Alert, TouchableOpacity, View } from "react-native"; +import { TvDetails } from "@/utils/jellyseerr/server/models/Tv"; +import { FlashList } from "@shopify/flash-list"; +import { orderBy } from "lodash"; +import { Tags } from "@/components/GenreTags"; import JellyseerrIconStatus from "@/components/icons/JellyseerrIconStatus"; import Season from "@/utils/jellyseerr/server/entity/Season"; -import {MediaStatus, MediaType} from "@/utils/jellyseerr/server/constants/media"; -import {Ionicons} from "@expo/vector-icons"; -import {RoundButton} from "@/components/RoundButton"; -import {useJellyseerr} from "@/hooks/useJellyseerr"; -import {TvResult} from "@/utils/jellyseerr/server/models/Search"; -import {useQuery} from "@tanstack/react-query"; -import {HorizontalScroll} from "@/components/common/HorrizontalScroll"; -import {Image} from "expo-image"; +import { + MediaStatus, + MediaType, +} from "@/utils/jellyseerr/server/constants/media"; +import { Ionicons } from "@expo/vector-icons"; +import { RoundButton } from "@/components/RoundButton"; +import { useJellyseerr } from "@/hooks/useJellyseerr"; +import { TvResult } from "@/utils/jellyseerr/server/models/Search"; +import { useQuery } from "@tanstack/react-query"; +import { HorizontalScroll } from "@/components/common/HorrizontalScroll"; +import { Image } from "expo-image"; import MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest"; +import { Loader } from "../Loader"; -const JellyseerrSeasonEpisodes: React.FC<{details: TvDetails, seasonNumber: number}> = ({ - details, - seasonNumber -}) => { - const {jellyseerrApi} = useJellyseerr(); +const JellyseerrSeasonEpisodes: React.FC<{ + details: TvDetails; + seasonNumber: number; +}> = ({ details, seasonNumber }) => { + const { jellyseerrApi } = useJellyseerr(); - const {data: seasonWithEpisodes, isLoading} = useQuery({ + const { data: seasonWithEpisodes, isLoading } = useQuery({ queryKey: ["jellyseerr", details.id, "season", seasonNumber], queryFn: async () => jellyseerrApi?.tvSeason(details.id, seasonNumber), - enabled: details.seasons.filter(s => s.seasonNumber !== 0).length > 0 - }) + enabled: details.seasons.filter((s) => s.seasonNumber !== 0).length > 0, + }); return ( item.id} - ItemSeparatorComponent={() => } renderItem={(item, index) => ( - - {item.stillPath && ( - - - - )} - - - {item?.name} - - - {`S${item?.seasonNumber}:E${item?.episodeNumber}`} - - - - - {item?.overview} - - + )} /> - ) -} + ); +}; + +const RenderItem = ({ item, index }: any) => { + const { jellyseerrApi } = useJellyseerr(); + const [imageError, setImageError] = useState(false); + + return ( + + + {!imageError ? ( + { + setImageError(true); + }} + /> + ) : ( + + + + )} + + + + {item.name} + + + {`S${item.seasonNumber}:E${item.episodeNumber}`} + + + + + {item.overview} + + + ); +}; const JellyseerrSeasons: React.FC<{ - isLoading: boolean, - result?: TvResult, - details?: TvDetails -}> = ({ - isLoading, - result, - details, -}) => { - if (!details) - return null; + isLoading: boolean; + result?: TvResult; + details?: TvDetails; +}> = ({ isLoading, result, details }) => { + if (!details) return null; - const {jellyseerrApi, requestMedia} = useJellyseerr(); - const [seasonStates, setSeasonStates] = useState<{[key: number]: boolean}>(); + const { jellyseerrApi, requestMedia } = useJellyseerr(); + const [seasonStates, setSeasonStates] = useState<{ + [key: number]: boolean; + }>(); const seasons = useMemo(() => { - const mediaInfoSeasons = details?.mediaInfo?.seasons?.filter((s: Season) => s.seasonNumber !== 0) - const requestedSeasons = details?.mediaInfo?.requests?.flatMap((r: MediaRequest) => r.seasons) - return details.seasons?.map((season) => { - return { - ...season, - status: - // What our library status is - mediaInfoSeasons - ?.find((mediaSeason: Season) => mediaSeason.seasonNumber === season.seasonNumber) - ?.status - ?? - // What our request status is - requestedSeasons - ?.find((s: Season) => s.seasonNumber === season.seasonNumber) - ?.status - ?? - // Otherwise set it as unknown - MediaStatus.UNKNOWN - } - }) - }, - [details] - ); + const mediaInfoSeasons = details?.mediaInfo?.seasons?.filter( + (s: Season) => s.seasonNumber !== 0 + ); + const requestedSeasons = details?.mediaInfo?.requests?.flatMap( + (r: MediaRequest) => r.seasons + ); + return details.seasons?.map((season) => { + return { + ...season, + status: + // What our library status is + mediaInfoSeasons?.find( + (mediaSeason: Season) => + mediaSeason.seasonNumber === season.seasonNumber + )?.status ?? + // What our request status is + requestedSeasons?.find( + (s: Season) => s.seasonNumber === season.seasonNumber + )?.status ?? + // Otherwise set it as unknown + MediaStatus.UNKNOWN, + }; + }); + }, [details]); - const allSeasonsAvailable = useMemo(() => - seasons?.every(season => season.status === MediaStatus.AVAILABLE), + const allSeasonsAvailable = useMemo( + () => seasons?.every((season) => season.status === MediaStatus.AVAILABLE), [seasons] - ) + ); const requestAll = useCallback(() => { if (details && jellyseerrApi) { @@ -125,48 +145,77 @@ const JellyseerrSeasons: React.FC<{ mediaType: MediaType.TV, tvdbId: details.externalIds?.tvdbId, seasons: seasons - .filter(s => s.status === MediaStatus.UNKNOWN && s.seasonNumber !== 0) - .map(s => s.seasonNumber) - }) + .filter( + (s) => s.status === MediaStatus.UNKNOWN && s.seasonNumber !== 0 + ) + .map((s) => s.seasonNumber), + }); } - }, [jellyseerrApi, seasons, details]) + }, [jellyseerrApi, seasons, details]); - const promptRequestAll = useCallback(() => ( - Alert.alert('Request all?', 'Are you sure you want to request all seasons?', [ - { - text: 'Cancel', - style: 'cancel', - }, - { - text: 'YES', - onPress: requestAll - }, - ])), [requestAll]); + const promptRequestAll = useCallback( + () => + Alert.alert( + "Request all?", + "Are you sure you want to request all seasons?", + [ + { + text: "Cancel", + style: "cancel", + }, + { + text: "YES", + onPress: requestAll, + }, + ] + ), + [requestAll] + ); + + if (isLoading) + return ( + + + Seasons + {!allSeasonsAvailable && ( + + + + )} + + + + ); return ( s.seasonNumber !== 0), 'seasonNumber', 'desc')} + data={orderBy( + details.seasons.filter((s) => s.seasonNumber !== 0), + "seasonNumber", + "desc" + )} ListHeaderComponent={() => ( - + Seasons {!allSeasonsAvailable && ( - - + + )} )} ItemSeparatorComponent={() => } estimatedItemSize={250} - renderItem={({item: season}) => ( + renderItem={({ item: season }) => ( <> setSeasonStates((prevState) => ( - {...prevState, [season.seasonNumber]: !prevState?.[season.seasonNumber]} - ))} + onPress={() => + setSeasonStates((prevState) => ({ + ...prevState, + [season.seasonNumber]: !prevState?.[season.seasonNumber], + })) + } + className="px-4" > {[0].map(() => { - const canRequest = seasons?.find(s => s.seasonNumber === season.seasonNumber)?.status === MediaStatus.UNKNOWN - return - requestMedia( - `${result?.name!!}, Season ${season.seasonNumber}`, - { - mediaId: details.id, - mediaType: MediaType.TV, - tvdbId: details.externalIds?.tvdbId, - seasons: [season.seasonNumber] - } - ) : undefined - } - className={canRequest ? 'bg-gray-700/40' : undefined} - mediaStatus={seasons?.find(s => s.seasonNumber === season.seasonNumber)?.status} - showRequestIcon={canRequest} - /> + const canRequest = + seasons?.find((s) => s.seasonNumber === season.seasonNumber) + ?.status === MediaStatus.UNKNOWN; + return ( + + requestMedia( + `${result?.name!!}, Season ${ + season.seasonNumber + }`, + { + mediaId: details.id, + mediaType: MediaType.TV, + tvdbId: details.externalIds?.tvdbId, + seasons: [season.seasonNumber], + } + ) + : undefined + } + className={canRequest ? "bg-gray-700/40" : undefined} + mediaStatus={ + seasons?.find( + (s) => s.seasonNumber === season.seasonNumber + )?.status + } + showRequestIcon={canRequest} + /> + ); })} @@ -206,10 +271,9 @@ const JellyseerrSeasons: React.FC<{ /> )} - ) - } + )} /> - ) -} + ); +}; -export default JellyseerrSeasons; \ No newline at end of file +export default JellyseerrSeasons; diff --git a/components/settings/MediaContext.tsx b/components/settings/MediaContext.tsx index c425b110..e0a81e6e 100644 --- a/components/settings/MediaContext.tsx +++ b/components/settings/MediaContext.tsx @@ -57,8 +57,6 @@ export const MediaProvider = ({ children }: { children: ReactNode }) => { updateSettings(update); - console.log("update", update); - let updatePayload = { SubtitleMode: update?.subtitleMode ?? settings?.subtitleMode, PlayDefaultAudioTrack: @@ -84,8 +82,6 @@ export const MediaProvider = ({ children }: { children: ReactNode }) => { settings?.defaultSubtitleLanguage?.ThreeLetterISOLanguageName || ""; - console.log("updatePayload", updatePayload); - updateUserConfiguration(updatePayload); }; diff --git a/components/video-player/controls/AudioSlider.tsx b/components/video-player/controls/AudioSlider.tsx index 6d579e0a..65ab7b9f 100644 --- a/components/video-player/controls/AudioSlider.tsx +++ b/components/video-player/controls/AudioSlider.tsx @@ -20,7 +20,6 @@ const AudioSlider: React.FC = ({ setVisibility }) => { const fetchInitialVolume = async () => { try { const { volume: initialVolume } = await VolumeManager.getVolume(); - console.log("initialVolume", initialVolume); volume.value = initialVolume * 100; } catch (error) { console.error("Error fetching initial volume:", error); @@ -39,7 +38,6 @@ const AudioSlider: React.FC = ({ setVisibility }) => { const handleValueChange = async (value: number) => { volume.value = value; - console.log("volume through slider", value); await VolumeManager.setVolume(value / 100); // Re-call showNativeVolumeUI to ensure the setting is applied on iOS @@ -48,7 +46,6 @@ const AudioSlider: React.FC = ({ setVisibility }) => { useEffect(() => { const volumeListener = VolumeManager.addVolumeListener((result) => { - console.log("Volume through device", result.volume); volume.value = result.volume * 100; setVisibility(true); diff --git a/components/video-player/controls/BrightnessSlider.tsx b/components/video-player/controls/BrightnessSlider.tsx index 3cbbd460..f7b0f392 100644 --- a/components/video-player/controls/BrightnessSlider.tsx +++ b/components/video-player/controls/BrightnessSlider.tsx @@ -14,7 +14,6 @@ const BrightnessSlider = () => { useEffect(() => { const fetchInitialBrightness = async () => { const initialBrightness = await Brightness.getBrightnessAsync(); - console.log("initialBrightness", initialBrightness); brightness.value = initialBrightness * 100; }; fetchInitialBrightness(); diff --git a/components/video-player/controls/Controls.tsx b/components/video-player/controls/Controls.tsx index 65206d8f..d7386134 100644 --- a/components/video-player/controls/Controls.tsx +++ b/components/video-player/controls/Controls.tsx @@ -240,8 +240,6 @@ export const Controls: React.FC = ({ ? maxValue - currentProgress : ticksToSeconds(maxValue - currentProgress); - console.log("remaining: ", remaining); - setCurrentTime(current); setRemainingTime(remaining); }, @@ -349,7 +347,6 @@ export const Controls: React.FC = ({ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); try { const curr = progress.value; - console.log(curr); if (curr !== undefined) { const newTime = isVlc ? curr + secondsToMs(settings.forwardSkipTime) @@ -375,8 +372,6 @@ export const Controls: React.FC = ({ const tileWidth = 150; const tileHeight = 150 / trickplayInfo.aspectRatio!; - console.log("time, ", time); - return ( = ({ }, (finished) => { if (finished && onFinish) { - console.log("finish"); runOnJS(onFinish)(); } } diff --git a/components/video-player/controls/dropdown/DropdownViewDirect.tsx b/components/video-player/controls/dropdown/DropdownViewDirect.tsx index 238e9775..28b55fa0 100644 --- a/components/video-player/controls/dropdown/DropdownViewDirect.tsx +++ b/components/video-player/controls/dropdown/DropdownViewDirect.tsx @@ -106,19 +106,12 @@ const DropdownViewDirect: React.FC = ({ if ("deliveryUrl" in sub && sub.deliveryUrl) { setSubtitleURL && setSubtitleURL(api?.basePath + sub.deliveryUrl, sub.name); - - console.log( - "Set external subtitle: ", - api?.basePath + sub.deliveryUrl - ); } else { - console.log("Set sub index: ", sub.index); setSubtitleTrack && setSubtitleTrack(sub.index); } router.setParams({ subtitleIndex: sub.index.toString(), }); - console.log("Subtitle: ", sub); }} > diff --git a/components/video-player/controls/dropdown/DropdownViewTranscoding.tsx b/components/video-player/controls/dropdown/DropdownViewTranscoding.tsx index 7bbe7e82..8739b07a 100644 --- a/components/video-player/controls/dropdown/DropdownViewTranscoding.tsx +++ b/components/video-player/controls/dropdown/DropdownViewTranscoding.tsx @@ -66,7 +66,6 @@ const DropdownView: React.FC = ({ showControls }) => { const sortedSubtitles = subtitleHelper.getSortedSubtitles(textSubtitles); - console.log("sortedSubtitles", sortedSubtitles); return [disableSubtitle, ...sortedSubtitles]; } @@ -104,7 +103,6 @@ const DropdownView: React.FC = ({ showControls }) => { const ChangeTranscodingAudio = useCallback( (audioIndex: number) => { - console.log("ChangeTranscodingAudio", subtitleIndex, audioIndex); const queryParams = new URLSearchParams({ itemId: item.Id ?? "", // Ensure itemId is a string audioIndex: audioIndex?.toString() ?? "", @@ -167,7 +165,6 @@ const DropdownView: React.FC = ({ showControls }) => { } key={`subtitle-item-${idx}`} onValueChange={() => { - console.log("sub", sub); if ( subtitleIndex === (isOnTextSubtitle && sub.IsTextSubtitleStream @@ -216,7 +213,6 @@ const DropdownView: React.FC = ({ showControls }) => { value={audioIndex === track.index.toString()} onValueChange={() => { if (audioIndex === track.index.toString()) return; - console.log("Setting audio track to: ", track.index); router.setParams({ audioIndex: track.index.toString(), }); diff --git a/hooks/useIntroSkipper.ts b/hooks/useIntroSkipper.ts index e6d28167..15aaff05 100644 --- a/hooks/useIntroSkipper.ts +++ b/hooks/useIntroSkipper.ts @@ -76,7 +76,6 @@ export const useIntroSkipper = ( }, [introTimestamps, currentTime]); const skipIntro = useCallback(() => { - console.log("skipIntro"); if (!introTimestamps) return; try { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); diff --git a/hooks/useJellyseerr.ts b/hooks/useJellyseerr.ts index e024720c..c0a8af22 100644 --- a/hooks/useJellyseerr.ts +++ b/hooks/useJellyseerr.ts @@ -1,45 +1,54 @@ -import axios, {AxiosError, AxiosInstance} from "axios"; -import {Results} from "@/utils/jellyseerr/server/models/Search"; +import axios, { AxiosError, AxiosInstance } from "axios"; +import { Results } from "@/utils/jellyseerr/server/models/Search"; import { storage } from "@/utils/mmkv"; -import {inRange} from "lodash"; -import {User as JellyseerrUser} from "@/utils/jellyseerr/server/entity/User"; -import {atom} from "jotai"; -import {useAtom} from "jotai/index"; +import { inRange } from "lodash"; +import { User as JellyseerrUser } from "@/utils/jellyseerr/server/entity/User"; +import { atom } from "jotai"; +import { useAtom } from "jotai/index"; import "@/augmentations"; -import {useCallback, useMemo} from "react"; -import {useSettings} from "@/utils/atoms/settings"; -import {toast} from "sonner-native"; -import {MediaRequestStatus, MediaType} from "@/utils/jellyseerr/server/constants/media"; +import { useCallback, useMemo } from "react"; +import { useSettings } from "@/utils/atoms/settings"; +import { toast } from "sonner-native"; +import { + MediaRequestStatus, + MediaType, +} from "@/utils/jellyseerr/server/constants/media"; import MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest"; -import {MediaRequestBody} from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces"; -import {MovieDetails} from "@/utils/jellyseerr/server/models/Movie"; -import {SeasonWithEpisodes, TvDetails} from "@/utils/jellyseerr/server/models/Tv"; -import {IssueStatus, IssueType} from "@/utils/jellyseerr/server/constants/issue"; +import { MediaRequestBody } from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces"; +import { MovieDetails } from "@/utils/jellyseerr/server/models/Movie"; +import { + SeasonWithEpisodes, + TvDetails, +} from "@/utils/jellyseerr/server/models/Tv"; +import { + IssueStatus, + IssueType, +} from "@/utils/jellyseerr/server/constants/issue"; import Issue from "@/utils/jellyseerr/server/entity/Issue"; -import {RTRating} from "@/utils/jellyseerr/server/api/rating/rottentomatoes"; -import {writeErrorLog} from "@/utils/log"; +import { RTRating } from "@/utils/jellyseerr/server/api/rating/rottentomatoes"; +import { writeErrorLog } from "@/utils/log"; import DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider"; interface SearchParams { - query: string, - page: number, + query: string; + page: number; language: string; } interface SearchResults { - page: number, - totalPages: number, + page: number; + totalPages: number; totalResults: number; results: Results[]; } -const JELLYSEERR_USER = "JELLYSEERR_USER" -const JELLYSEERR_COOKIES = "JELLYSEERR_COOKIES" +const JELLYSEERR_USER = "JELLYSEERR_USER"; +const JELLYSEERR_COOKIES = "JELLYSEERR_COOKIES"; export const clearJellyseerrStorageData = () => { storage.delete(JELLYSEERR_USER); storage.delete(JELLYSEERR_COOKIES); -} +}; export enum Endpoints { STATUS = "/status", @@ -58,24 +67,29 @@ export enum Endpoints { AUTH_JELLYFIN = "/auth/jellyfin", } -export type DiscoverEndpoint = Endpoints.DISCOVER_TRENDING | Endpoints.DISCOVER_MOVIES | Endpoints.DISCOVER_TV; +export type DiscoverEndpoint = + | Endpoints.DISCOVER_TRENDING + | Endpoints.DISCOVER_MOVIES + | Endpoints.DISCOVER_TV; -export type TestResult = { - isValid: true; - requiresPass: boolean; -} | { - isValid: false; -}; +export type TestResult = + | { + isValid: true; + requiresPass: boolean; + } + | { + isValid: false; + }; export class JellyseerrApi { axios: AxiosInstance; - constructor (baseUrl: string) { + constructor(baseUrl: string) { this.axios = axios.create({ baseURL: baseUrl, withCredentials: true, withXSRFToken: true, - xsrfHeaderName: "XSRF-TOKEN" + xsrfHeaderName: "XSRF-TOKEN", }); this.setInterceptors(); @@ -86,132 +100,169 @@ export class JellyseerrApi { const cookies = storage.get(JELLYSEERR_COOKIES); if (user && cookies) { - console.log("User & cookies data exist for jellyseerr") return Promise.resolve({ isValid: true, - requiresPass: false + requiresPass: false, }); } - console.log("Testing jellyseerr connection") - return await this.axios.get(Endpoints.API_V1 + Endpoints.STATUS) + return await this.axios + .get(Endpoints.API_V1 + Endpoints.STATUS) .then((response) => { - const {status, headers, data} = response; + const { status, headers, data } = response; if (inRange(status, 200, 299)) { if (data.version < "2.0.0") { - const error = "Jellyseerr server does not meet minimum version requirements! Please update to at least 2.0.0"; + const error = + "Jellyseerr server does not meet minimum version requirements! Please update to at least 2.0.0"; toast.error(error); throw Error(error); } - console.log("Jellyseerr connecting successfully tested!"); - storage.setAny(JELLYSEERR_COOKIES, headers["set-cookie"]?.flatMap(c => c.split("; ")) ?? []); + storage.setAny( + JELLYSEERR_COOKIES, + headers["set-cookie"]?.flatMap((c) => c.split("; ")) ?? [] + ); return { isValid: true, - requiresPass: true + requiresPass: true, }; } toast.error(`Jellyseerr test failed. Please try again.`); writeErrorLog( `Jellyseerr returned a ${status} for url:\n` + - response.config.url + '\n' + - JSON.stringify(response.data) + response.config.url + + "\n" + + JSON.stringify(response.data) ); return { isValid: false, - requiresPass: false + requiresPass: false, }; }) .catch((e) => { const msg = "Failed to test jellyseerr server url"; - toast.error(msg) - console.error(msg, e) + toast.error(msg); + console.error(msg, e); return { isValid: false, - requiresPass: false + requiresPass: false, }; - }) + }); } async login(username: string, password: string): Promise { - return this.axios?.post(Endpoints.API_V1 + Endpoints.AUTH_JELLYFIN, { - username, - password, - email: username - }).then(response => { - const user = response?.data; - if (!user) - throw Error("Login failed") - storage.setAny(JELLYSEERR_USER, user); - return user - }) + return this.axios + ?.post(Endpoints.API_V1 + Endpoints.AUTH_JELLYFIN, { + username, + password, + email: username, + }) + .then((response) => { + const user = response?.data; + if (!user) throw Error("Login failed"); + storage.setAny(JELLYSEERR_USER, user); + return user; + }); } async discoverSettings(): Promise { - return this.axios?.get(Endpoints.API_V1 + Endpoints.SETTINGS + Endpoints.DISCOVER) - .then(({data}) => data) + return this.axios + ?.get( + Endpoints.API_V1 + Endpoints.SETTINGS + Endpoints.DISCOVER + ) + .then(({ data }) => data); } - async discover(endpoint: DiscoverEndpoint, params: any): Promise { - return this.axios?.get(Endpoints.API_V1 + endpoint, { params }) - .then(({data}) => data) + async discover( + endpoint: DiscoverEndpoint, + params: any + ): Promise { + return this.axios + ?.get(Endpoints.API_V1 + endpoint, { params }) + .then(({ data }) => data); } async search(params: SearchParams): Promise { - const response = await this.axios?.get(Endpoints.API_V1 + Endpoints.SEARCH, {params}) - return response?.data + const response = await this.axios?.get( + Endpoints.API_V1 + Endpoints.SEARCH, + { params } + ); + return response?.data; } async request(request: MediaRequestBody): Promise { - return this.axios?.post(Endpoints.API_V1 + Endpoints.REQUEST, request) - .then(({data}) => data) + return this.axios + ?.post(Endpoints.API_V1 + Endpoints.REQUEST, request) + .then(({ data }) => data); } async movieDetails(id: number) { - return this.axios?.get(Endpoints.API_V1 + Endpoints.MOVIE + `/${id}`).then(response => { - return response?.data - }) + return this.axios + ?.get(Endpoints.API_V1 + Endpoints.MOVIE + `/${id}`) + .then((response) => { + return response?.data; + }); } - async movieRatings(id: number) { - return this.axios?.get(`${Endpoints.API_V1}${Endpoints.MOVIE}/${id}${Endpoints.RATINGS}`) - .then(({data}) => data) + return this.axios + ?.get( + `${Endpoints.API_V1}${Endpoints.MOVIE}/${id}${Endpoints.RATINGS}` + ) + .then(({ data }) => data); } async tvDetails(id: number) { - return this.axios?.get(`${Endpoints.API_V1}${Endpoints.TV}/${id}`).then(response => { - return response?.data - }) + return this.axios + ?.get(`${Endpoints.API_V1}${Endpoints.TV}/${id}`) + .then((response) => { + return response?.data; + }); } async tvRatings(id: number) { - return this.axios?.get(`${Endpoints.API_V1}${Endpoints.TV}/${id}${Endpoints.RATINGS}`) - .then(({data}) => data) + return this.axios + ?.get( + `${Endpoints.API_V1}${Endpoints.TV}/${id}${Endpoints.RATINGS}` + ) + .then(({ data }) => data); } async tvSeason(id: number, seasonId: number) { - return this.axios?.get(`${Endpoints.API_V1}${Endpoints.TV}/${id}/season/${seasonId}`).then(response => { - console.log(response.data.episodes) - return response?.data - }) + return this.axios + ?.get( + `${Endpoints.API_V1}${Endpoints.TV}/${id}/season/${seasonId}` + ) + .then((response) => { + return response?.data; + }); } tvStillImageProxy(path: string, width: number = 1920, quality: number = 75) { - return this.axios.defaults.baseURL + `/_next/image?` + new URLSearchParams(`url=https://image.tmdb.org/t/p/original/${path}&w=${width}&q=${quality}`).toString() + return ( + this.axios.defaults.baseURL + + `/_next/image?` + + new URLSearchParams( + `url=https://image.tmdb.org/t/p/original/${path}&w=${width}&q=${quality}` + ).toString() + ); } async submitIssue(mediaId: number, issueType: IssueType, message: string) { - return this.axios?.post(Endpoints.API_V1 + Endpoints.ISSUE, { - mediaId, issueType, message - }).then((response) => { - const issue = response.data + return this.axios + ?.post(Endpoints.API_V1 + Endpoints.ISSUE, { + mediaId, + issueType, + message, + }) + .then((response) => { + const issue = response.data; - if (issue.status === IssueStatus.OPEN) { - toast.success("Issue submitted!") - } - return issue - }) + if (issue.status === IssueStatus.OPEN) { + toast.success("Issue submitted!"); + } + return issue; + }); } private setInterceptors() { @@ -219,7 +270,10 @@ export class JellyseerrApi { async (response) => { const cookies = response.headers["set-cookie"]; if (cookies) { - storage.setAny(JELLYSEERR_COOKIES, response.headers["set-cookie"]?.flatMap(c => c.split("; "))); + storage.setAny( + JELLYSEERR_COOKIES, + response.headers["set-cookie"]?.flatMap((c) => c.split("; ")) + ); } return response; }, @@ -227,16 +281,17 @@ export class JellyseerrApi { const errorMsg = "Jellyseerr response error"; console.error(errorMsg, error, error.response?.data); writeErrorLog( - errorMsg + `\n` + - `error: ${error.toString()}\n` + - `url: ${error?.config?.url}\n` + - `data:\n` + - JSON.stringify(error.response?.data) + errorMsg + + `\n` + + `error: ${error.toString()}\n` + + `url: ${error?.config?.url}\n` + + `data:\n` + + JSON.stringify(error.response?.data) ); if (error.status === 403) { - clearJellyseerrStorageData() + clearJellyseerrStorageData(); } - return Promise.reject(error) + return Promise.reject(error); } ); @@ -246,22 +301,22 @@ export class JellyseerrApi { if (cookies) { const headerName = this.axios.defaults.xsrfHeaderName!!; const xsrfToken = cookies - .find(c => c.includes(headerName)) - ?.split(headerName + "=")?.[1] + .find((c) => c.includes(headerName)) + ?.split(headerName + "=")?.[1]; if (xsrfToken) { config.headers[headerName] = xsrfToken; } } - return config + return config; }, (error) => { - console.error("Jellyseerr request error", error) + console.error("Jellyseerr request error", error); } ); } } -const jellyseerrUserAtom = atom(storage.get(JELLYSEERR_USER)) +const jellyseerrUserAtom = atom(storage.get(JELLYSEERR_USER)); export const useJellyseerr = () => { const [jellyseerrUser, setJellyseerrUser] = useAtom(jellyseerrUserAtom); @@ -270,43 +325,47 @@ export const useJellyseerr = () => { const jellyseerrApi = useMemo(() => { const cookies = storage.get(JELLYSEERR_COOKIES); if (settings?.jellyseerrServerUrl && cookies && jellyseerrUser) { - return new JellyseerrApi(settings?.jellyseerrServerUrl) + return new JellyseerrApi(settings?.jellyseerrServerUrl); } - return undefined - }, [settings?.jellyseerrServerUrl, jellyseerrUser]) + return undefined; + }, [settings?.jellyseerrServerUrl, jellyseerrUser]); const clearAllJellyseerData = useCallback(async () => { - clearJellyseerrStorageData() + clearJellyseerrStorageData(); setJellyseerrUser(undefined); - updateSettings({jellyseerrServerUrl: undefined}) + updateSettings({ jellyseerrServerUrl: undefined }); }, []); - const requestMedia = useCallback(( - title: string, - request: MediaRequestBody, - ) => { - jellyseerrApi?.request?.(request)?.then((mediaRequest) => { - switch (mediaRequest.status) { - case MediaRequestStatus.PENDING: - case MediaRequestStatus.APPROVED: - toast.success(`Requested ${title}!`) - break; - case MediaRequestStatus.DECLINED: - toast.error(`You don't have permission to request!`) - break; - case MediaRequestStatus.FAILED: - toast.error(`Something went wrong requesting media!`) - break; - } - }) - }, [jellyseerrApi]) + const requestMedia = useCallback( + (title: string, request: MediaRequestBody) => { + jellyseerrApi?.request?.(request)?.then((mediaRequest) => { + switch (mediaRequest.status) { + case MediaRequestStatus.PENDING: + case MediaRequestStatus.APPROVED: + toast.success(`Requested ${title}!`); + break; + case MediaRequestStatus.DECLINED: + toast.error(`You don't have permission to request!`); + break; + case MediaRequestStatus.FAILED: + toast.error(`Something went wrong requesting media!`); + break; + } + }); + }, + [jellyseerrApi] + ); - const isJellyseerrResult = (items: any[] | null | undefined): items is Results[] => { + const isJellyseerrResult = ( + items: any[] | null | undefined + ): items is Results[] => { return ( !items || - items.length >= 0 && Object.hasOwn(items[0], "mediaType") && Object.values(MediaType).includes(items[0]['mediaType']) - ) - } + (items.length >= 0 && + Object.hasOwn(items[0], "mediaType") && + Object.values(MediaType).includes(items[0]["mediaType"])) + ); + }; return { jellyseerrApi, @@ -314,6 +373,6 @@ export const useJellyseerr = () => { setJellyseerrUser, clearAllJellyseerData, isJellyseerrResult, - requestMedia - } + requestMedia, + }; }; From a936916da4dd5dc3cad0ea9b72843dcb06ee44f3 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Tue, 31 Dec 2024 13:35:59 +0100 Subject: [PATCH 11/19] fix: design --- app/(auth)/(tabs)/(search)/index.tsx | 143 +++++++++++++++--------- components/jellyseerr/DiscoverSlide.tsx | 107 +++++++++++------- 2 files changed, 156 insertions(+), 94 deletions(-) diff --git a/app/(auth)/(tabs)/(search)/index.tsx b/app/(auth)/(tabs)/(search)/index.tsx index 979119a6..ac3d83fe 100644 --- a/app/(auth)/(tabs)/(search)/index.tsx +++ b/app/(auth)/(tabs)/(search)/index.tsx @@ -30,15 +30,15 @@ import React, { import { Platform, ScrollView, TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useDebounce } from "use-debounce"; -import {useJellyseerr} from "@/hooks/useJellyseerr"; -import {MovieResult, TvResult} from "@/utils/jellyseerr/server/models/Search"; -import {MediaType} from "@/utils/jellyseerr/server/constants/media"; +import { useJellyseerr } from "@/hooks/useJellyseerr"; +import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search"; +import { MediaType } from "@/utils/jellyseerr/server/constants/media"; import JellyseerrPoster from "@/components/posters/JellyseerrPoster"; -import {Tag} from "@/components/GenreTags"; +import { Tag } from "@/components/GenreTags"; import DiscoverSlide from "@/components/jellyseerr/DiscoverSlide"; -import {sortBy} from "lodash"; +import { sortBy } from "lodash"; -type SearchType = 'Library' | 'Discover'; +type SearchType = "Library" | "Discover"; const exampleSearches = [ "Lord of the rings", @@ -153,29 +153,41 @@ export default function search() { const response = await jellyseerrApi?.search({ query: new URLSearchParams(debouncedSearch).toString(), page: 1, // todo: maybe rework page & page-size if first results are not enough... - language: 'en' - }) + language: "en", + }); return response?.results; }, - enabled: !!jellyseerrApi && searchType === "Discover" && debouncedSearch.length > 0, + enabled: + !!jellyseerrApi && + searchType === "Discover" && + debouncedSearch.length > 0, }); const { data: jellyseerrDiscoverSettings, isFetching: j2 } = useQuery({ queryKey: ["search", "jellyseerrDiscoverSettings", debouncedSearch], queryFn: async () => jellyseerrApi?.discoverSettings(), - enabled: !!jellyseerrApi && searchType === "Discover" && debouncedSearch.length == 0, + enabled: + !!jellyseerrApi && + searchType === "Discover" && + debouncedSearch.length == 0, }); - const jellyseerrMovieResults: MovieResult[] | undefined = useMemo(() => - jellyseerrResults?.filter(r => r.mediaType === MediaType.MOVIE) as MovieResult[], + const jellyseerrMovieResults: MovieResult[] | undefined = useMemo( + () => + jellyseerrResults?.filter( + (r) => r.mediaType === MediaType.MOVIE + ) as MovieResult[], [jellyseerrResults] - ) + ); - const jellyseerrTvResults: TvResult[] | undefined = useMemo(() => - jellyseerrResults?.filter(r => r.mediaType === MediaType.TV) as TvResult[], + const jellyseerrTvResults: TvResult[] | undefined = useMemo( + () => + jellyseerrResults?.filter( + (r) => r.mediaType === MediaType.TV + ) as TvResult[], [jellyseerrResults] - ) + ); const { data: series, isFetching: l2 } = useQuery({ queryKey: ["search", "series", debouncedSearch], @@ -260,7 +272,17 @@ export default function search() { jellyseerrMovieResults?.length || jellyseerrTvResults?.length ); - }, [artists, episodes, albums, songs, movies, series, collections, actors, jellyseerrResults]); + }, [ + artists, + episodes, + albums, + songs, + movies, + series, + collections, + actors, + jellyseerrResults, + ]); const loading = useMemo(() => { return l1 || l2 || l3 || l4 || l5 || l6 || l7 || l8 || j1 || j2; @@ -290,14 +312,24 @@ export default function search() { )} {jellyseerrApi && ( - - setSearchType('Library')}> - + + setSearchType("Library")}> + - setSearchType('Discover')}> - + setSearchType("Discover")}> + )} @@ -319,7 +351,7 @@ export default function search() { className="flex flex-col w-28 mr-2" item={item} > - + {item.Name} @@ -338,7 +370,7 @@ export default function search() { item={item} className="flex flex-col w-28 mr-2" > - + {item.Name} @@ -357,8 +389,8 @@ export default function search() { key={item.Id} className="flex flex-col w-44 mr-2" > - - + + )} /> @@ -371,7 +403,7 @@ export default function search() { item={item} className="flex flex-col w-28 mr-2" > - + {item.Name} @@ -387,8 +419,8 @@ export default function search() { key={item.Id} className="flex flex-col w-28 mr-2" > - - + + )} /> @@ -401,8 +433,8 @@ export default function search() { key={item.Id} className="flex flex-col w-28 mr-2" > - - + + )} /> @@ -415,8 +447,8 @@ export default function search() { key={item.Id} className="flex flex-col w-28 mr-2" > - - + + )} /> @@ -429,8 +461,8 @@ export default function search() { key={item.Id} className="flex flex-col w-28 mr-2" > - - + + )} /> @@ -442,14 +474,14 @@ export default function search() { header="Request Movies" items={jellyseerrMovieResults} renderItem={(item: MovieResult) => ( - + )} /> ( - + )} /> @@ -468,7 +500,7 @@ export default function search() { "{debouncedSearch}" - ) : debouncedSearch.length === 0 && searchType === 'Library' ? ( + ) : debouncedSearch.length === 0 && searchType === "Library" ? ( {exampleSearches.map((e) => ( ))} - ) : debouncedSearch.length === 0 && searchType === 'Discover' ? ( + ) : debouncedSearch.length === 0 && searchType === "Discover" ? ( - {sortBy?.(jellyseerrDiscoverSettings?.filter(s => s.enabled), 'order') - .map((slide) => ) - } + {sortBy?.( + jellyseerrDiscoverSettings?.filter((s) => s.enabled), + "order" + ).map((slide) => ( + + ))} ) : null} @@ -500,7 +535,12 @@ type Props = { header?: string; }; -const SearchItemWrapper = ({ ids, items, renderItem, header }: PropsWithChildren>) => { +const SearchItemWrapper = ({ + ids, + items, + renderItem, + header, +}: PropsWithChildren>) => { const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); @@ -540,14 +580,11 @@ const SearchItemWrapper = ({ ids, items, renderItem, header className="px-4 mb-2" showsHorizontalScrollIndicator={false} > - { - data && data?.length > 0 - ? data.map((item) => renderItem(item)) - : - items && items?.length > 0 - ? items.map(i => renderItem(i)) - : undefined - } + {data && data?.length > 0 + ? data.map((item) => renderItem(item)) + : items && items?.length > 0 + ? items.map((i) => renderItem(i)) + : undefined} ); diff --git a/components/jellyseerr/DiscoverSlide.tsx b/components/jellyseerr/DiscoverSlide.tsx index ca0f1d6e..7e566630 100644 --- a/components/jellyseerr/DiscoverSlide.tsx +++ b/components/jellyseerr/DiscoverSlide.tsx @@ -1,26 +1,31 @@ -import React, {useMemo} from "react"; +import React, { useMemo } from "react"; import DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider"; -import {DiscoverSliderType} from "@/utils/jellyseerr/server/constants/discover"; -import {DiscoverEndpoint, Endpoints, useJellyseerr} from "@/hooks/useJellyseerr"; -import {useInfiniteQuery} from "@tanstack/react-query"; -import {MovieResult, TvResult} from "@/utils/jellyseerr/server/models/Search"; +import { DiscoverSliderType } from "@/utils/jellyseerr/server/constants/discover"; +import { + DiscoverEndpoint, + Endpoints, + useJellyseerr, +} from "@/hooks/useJellyseerr"; +import { useInfiniteQuery } from "@tanstack/react-query"; +import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search"; import JellyseerrPoster from "@/components/posters/JellyseerrPoster"; -import {Text} from "@/components/common/Text"; -import {FlashList} from "@shopify/flash-list"; +import { Text } from "@/components/common/Text"; +import { FlashList } from "@shopify/flash-list"; +import { View } from "react-native"; interface Props { - slide: DiscoverSlider + slide: DiscoverSlider; } -const DiscoverSlide: React.FC = ({slide}) => { - const {jellyseerrApi} = useJellyseerr(); +const DiscoverSlide: React.FC = ({ slide }) => { + const { jellyseerrApi } = useJellyseerr(); - const {data, isFetching, fetchNextPage, hasNextPage} = useInfiniteQuery({ + const { data, isFetching, fetchNextPage, hasNextPage } = useInfiniteQuery({ queryKey: ["jellyseerr", "discover", slide.id], queryFn: async ({ pageParam }) => { let endpoint: DiscoverEndpoint | undefined = undefined; let params: any = { - page: Number(pageParam) - } + page: Number(pageParam), + }; switch (slide.type) { case DiscoverSliderType.TRENDING: @@ -28,48 +33,68 @@ const DiscoverSlide: React.FC = ({slide}) => { break; case DiscoverSliderType.POPULAR_MOVIES: case DiscoverSliderType.UPCOMING_MOVIES: - endpoint = Endpoints.DISCOVER_MOVIES + endpoint = Endpoints.DISCOVER_MOVIES; if (slide.type === DiscoverSliderType.UPCOMING_MOVIES) - params = { ...params, primaryReleaseDateGte: new Date().toISOString().split('T')[0]} + params = { + ...params, + primaryReleaseDateGte: new Date().toISOString().split("T")[0], + }; break; case DiscoverSliderType.POPULAR_TV: case DiscoverSliderType.UPCOMING_TV: - endpoint = Endpoints.DISCOVER_TV + endpoint = Endpoints.DISCOVER_TV; if (slide.type === DiscoverSliderType.UPCOMING_TV) - params = {...params, firstAirDateGte: new Date().toISOString().split('T')[0]} + params = { + ...params, + firstAirDateGte: new Date().toISOString().split("T")[0], + }; break; } return endpoint ? jellyseerrApi?.discover(endpoint, params) : null; }, initialPageParam: 1, - getNextPageParam: (lastPage, pages) => ((lastPage?.page || pages?.findLast(p => p?.results.length)?.page) || 1) + 1, + getNextPageParam: (lastPage, pages) => + (lastPage?.page || pages?.findLast((p) => p?.results.length)?.page || 1) + + 1, enabled: !!jellyseerrApi, - staleTime: 0 + staleTime: 0, }); - const flatData = useMemo(() => data?.pages?.filter(p => p?.results.length).flatMap(p => p?.results), [data]) + const flatData = useMemo( + () => + data?.pages?.filter((p) => p?.results.length).flatMap((p) => p?.results), + [data] + ); return ( - (flatData && flatData?.length > 0) && <> - {DiscoverSliderType[slide.type].toString().toTitle()} - item!!.id.toString()} - estimatedItemSize={250} - data={flatData} - onEndReachedThreshold={1} - onEndReached={() => { - if (hasNextPage) - fetchNextPage() - }} - renderItem={({item}) => - (item ? : <>) - } - /> - - ) -} + flatData && + flatData?.length > 0 && ( + + + {DiscoverSliderType[slide.type].toString().toTitle()} + + item!!.id.toString()} + estimatedItemSize={250} + data={flatData} + onEndReachedThreshold={1} + onEndReached={() => { + if (hasNextPage) fetchNextPage(); + }} + renderItem={({ item }) => + item ? ( + + ) : ( + <> + ) + } + /> + + ) + ); +}; -export default DiscoverSlide; \ No newline at end of file +export default DiscoverSlide; From 0f694480818dafc1c703218930cf9cefbfc79f2e Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Tue, 31 Dec 2024 13:40:23 +0100 Subject: [PATCH 12/19] fix: design --- .../jellyseerr/page.tsx | 2 +- components/series/JellyseerrSeasons.tsx | 24 ++++++++----------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/app/(auth)/(tabs)/(home,libraries,search)/jellyseerr/page.tsx b/app/(auth)/(tabs)/(home,libraries,search)/jellyseerr/page.tsx index 4acd8053..edf91697 100644 --- a/app/(auth)/(tabs)/(home,libraries,search)/jellyseerr/page.tsx +++ b/app/(auth)/(tabs)/(home,libraries,search)/jellyseerr/page.tsx @@ -153,7 +153,7 @@ const Page: React.FC = () => { > - + diff --git a/components/series/JellyseerrSeasons.tsx b/components/series/JellyseerrSeasons.tsx index b2a0d2ce..f0efa418 100644 --- a/components/series/JellyseerrSeasons.tsx +++ b/components/series/JellyseerrSeasons.tsx @@ -155,20 +155,16 @@ const JellyseerrSeasons: React.FC<{ const promptRequestAll = useCallback( () => - Alert.alert( - "Request all?", - "Are you sure you want to request all seasons?", - [ - { - text: "Cancel", - style: "cancel", - }, - { - text: "YES", - onPress: requestAll, - }, - ] - ), + Alert.alert("Confirm", "Are you sure you want to request all seasons?", [ + { + text: "Cancel", + style: "cancel", + }, + { + text: "Yes", + onPress: requestAll, + }, + ]), [requestAll] ); From 8344d4025bc723a739307b82a228a1a5a1cee33a Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Tue, 31 Dec 2024 14:54:31 +0100 Subject: [PATCH 13/19] fix: not possible to select seasons without index --- components/series/SeasonDropdown.tsx | 2 +- components/series/SeasonPicker.tsx | 65 ++++++++++++++++++++-------- 2 files changed, 47 insertions(+), 20 deletions(-) diff --git a/components/series/SeasonDropdown.tsx b/components/series/SeasonDropdown.tsx index 574048d4..5c333f2e 100644 --- a/components/series/SeasonDropdown.tsx +++ b/components/series/SeasonDropdown.tsx @@ -19,7 +19,7 @@ type SeasonKeys = { }; export type SeasonIndexState = { - [seriesId: string]: number | null | undefined; + [seriesId: string]: number | string | null | undefined; }; export const SeasonDropdown: React.FC = ({ diff --git a/components/series/SeasonPicker.tsx b/components/series/SeasonPicker.tsx index 00093cd4..adb40bac 100644 --- a/components/series/SeasonPicker.tsx +++ b/components/series/SeasonPicker.tsx @@ -30,7 +30,10 @@ export const SeasonPicker: React.FC = ({ item, initialSeasonIndex }) => { const [user] = useAtom(userAtom); const [seasonIndexState, setSeasonIndexState] = useAtom(seasonIndexAtom); - const seasonIndex = seasonIndexState[item.Id ?? ""]; + const seasonIndex = useMemo( + () => seasonIndexState[item.Id ?? ""], + [item, seasonIndexState] + ); const { data: seasons } = useQuery({ queryKey: ["seasons", item.Id], @@ -56,16 +59,24 @@ export const SeasonPicker: React.FC = ({ item, initialSeasonIndex }) => { enabled: !!api && !!user?.Id && !!item.Id, }); - const selectedSeasonId: string | null = useMemo( - () => - seasons?.find((season: any) => season.IndexNumber === seasonIndex)?.Id, - [seasons, seasonIndex] - ); + const selectedSeasonId: string | null = useMemo(() => { + const season: BaseItemDto = seasons?.find( + (s: BaseItemDto) => + s.IndexNumber === seasonIndex || s.Name === seasonIndex + ); + + if (!season?.Id) return null; + + return season.Id!; + }, [seasons, seasonIndex]); const { data: episodes, isFetching } = useQuery({ queryKey: ["episodes", item.Id, selectedSeasonId], queryFn: async () => { - if (!api || !user?.Id || !item.Id || !selectedSeasonId) return []; + if (!api || !user?.Id || !item.Id || !selectedSeasonId) { + return []; + } + const res = await getTvShowsApi(api).getEpisodes({ seriesId: item.Id, userId: user.Id, @@ -74,6 +85,12 @@ export const SeasonPicker: React.FC = ({ item, initialSeasonIndex }) => { fields: ["MediaSources", "MediaStreams", "Overview"], }); + if (res.data.TotalRecordCount === 0) + console.warn( + "No episodes found for season with ID ~", + selectedSeasonId + ); + return res.data.Items; }, enabled: !!api && !!user?.Id && !!item.Id && !!selectedSeasonId, @@ -118,23 +135,26 @@ export const SeasonPicker: React.FC = ({ item, initialSeasonIndex }) => { seasons={seasons} state={seasonIndexState} onSelect={(season) => { + if (!item.Id) return; setSeasonIndexState((prev) => ({ ...prev, - [item.Id ?? ""]: season.IndexNumber, + [item.Id!]: season.IndexNumber ?? season.Name, })); }} /> - ( - - )} - DownloadedIconComponent={() => ( - - )} - /> + {episodes?.length || 0 > 0 ? ( + ( + + )} + DownloadedIconComponent={() => ( + + )} + /> + ) : null} {isFetching ? ( @@ -186,6 +206,13 @@ export const SeasonPicker: React.FC = ({ item, initialSeasonIndex }) => { )) )} + {episodes?.length || 0 > 0 ? ( + + + No episodes for this season + + + ) : null} ); From 28568cbb9c55c413535d8b9755e5966f5be64c37 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Tue, 31 Dec 2024 14:56:41 +0100 Subject: [PATCH 14/19] fix: incorrect logic --- components/series/SeasonPicker.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/series/SeasonPicker.tsx b/components/series/SeasonPicker.tsx index adb40bac..d823c6c2 100644 --- a/components/series/SeasonPicker.tsx +++ b/components/series/SeasonPicker.tsx @@ -206,7 +206,7 @@ export const SeasonPicker: React.FC = ({ item, initialSeasonIndex }) => { )) )} - {episodes?.length || 0 > 0 ? ( + {(episodes?.length || 0) === 0 ? ( No episodes for this season From cccb26c9cca27c90a3a742d5c60ac243303cbdec Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Tue, 31 Dec 2024 15:00:30 +0100 Subject: [PATCH 15/19] chore --- components/series/SeasonPicker.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/series/SeasonPicker.tsx b/components/series/SeasonPicker.tsx index d823c6c2..d5fbabe9 100644 --- a/components/series/SeasonPicker.tsx +++ b/components/series/SeasonPicker.tsx @@ -156,7 +156,7 @@ export const SeasonPicker: React.FC = ({ item, initialSeasonIndex }) => { /> ) : null} - + {isFetching ? ( Date: Tue, 31 Dec 2024 16:01:19 +0100 Subject: [PATCH 16/19] fix: poster not clickable --- components/series/NextUp.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/components/series/NextUp.tsx b/components/series/NextUp.tsx index ce87bb19..aef8f3f9 100644 --- a/components/series/NextUp.tsx +++ b/components/series/NextUp.tsx @@ -11,6 +11,7 @@ import { Text } from "../common/Text"; import ContinueWatchingPoster from "../ContinueWatchingPoster"; import { ItemCardText } from "../ItemCardText"; import { TouchableItemRouter } from "../common/TouchableItemRouter"; +import { FlashList } from "@shopify/flash-list"; export const NextUp: React.FC<{ seriesId: string }> = ({ seriesId }) => { const [user] = useAtom(userAtom); @@ -44,9 +45,13 @@ export const NextUp: React.FC<{ seriesId: string }> = ({ seriesId }) => { return ( Next up - ( + renderItem={({ item, index }) => ( Date: Tue, 31 Dec 2024 16:01:30 +0100 Subject: [PATCH 17/19] feat: series info and trailer --- .../(home,libraries,search)/series/[id].tsx | 9 +-- app/(auth)/trailer/page.tsx | 53 +++++++++++++++ app/_layout.tsx | 7 ++ bun.lockb | Bin 588613 -> 590056 bytes components/Ratings.tsx | 1 + components/series/SeasonPicker.tsx | 1 + components/series/SeriesActions.tsx | 32 +++++++++ components/series/SeriesHeader.tsx | 64 ++++++++++++++++++ package.json | 2 + 9 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 app/(auth)/trailer/page.tsx create mode 100644 components/series/SeriesActions.tsx create mode 100644 components/series/SeriesHeader.tsx diff --git a/app/(auth)/(tabs)/(home,libraries,search)/series/[id].tsx b/app/(auth)/(tabs)/(home,libraries,search)/series/[id].tsx index ecee672b..e9c9fdea 100644 --- a/app/(auth)/(tabs)/(home,libraries,search)/series/[id].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search)/series/[id].tsx @@ -1,8 +1,11 @@ import { Text } from "@/components/common/Text"; import { DownloadItems } from "@/components/DownloadItem"; import { ParallaxScrollView } from "@/components/ParallaxPage"; +import { Ratings } from "@/components/Ratings"; import { NextUp } from "@/components/series/NextUp"; import { SeasonPicker } from "@/components/series/SeasonPicker"; +import { SeriesActions } from "@/components/series/SeriesActions"; +import { SeriesHeader } from "@/components/series/SeriesHeader"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl"; import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById"; @@ -70,6 +73,7 @@ const page: React.FC = () => { }); return res?.data.Items || []; }, + staleTime: 60, enabled: !!api && !!user?.Id && !!item?.Id, }); @@ -133,10 +137,7 @@ const page: React.FC = () => { } > - - {item?.Name} - {item?.Overview} - + diff --git a/app/(auth)/trailer/page.tsx b/app/(auth)/trailer/page.tsx new file mode 100644 index 00000000..c2a84809 --- /dev/null +++ b/app/(auth)/trailer/page.tsx @@ -0,0 +1,53 @@ +import { useGlobalSearchParams, useNavigation } from "expo-router"; +import { useState, useCallback, useEffect, useMemo } from "react"; +import { Button, Dimensions } from "react-native"; +import { Alert, View } from "react-native"; +import YoutubePlayer, { PLAYER_STATES } from "react-native-youtube-iframe"; + +export default function page() { + const searchParams = useGlobalSearchParams(); + const navigation = useNavigation(); + console.log(searchParams); + + const { url } = searchParams as { url: string }; + + const videoId = useMemo(() => { + return url.split("v=")[1]; + }, [url]); + + const [playing, setPlaying] = useState(false); + + const onStateChange = useCallback((state: PLAYER_STATES) => { + if (state === "ended") { + setPlaying(false); + Alert.alert("video has finished playing!"); + } + }, []); + + const togglePlaying = useCallback(() => { + setPlaying((prev) => !prev); + }, []); + + useEffect(() => { + navigation.setOptions({ + headerShown: false, + }); + + togglePlaying(); + }, []); + + const screenWidth = Dimensions.get("screen").width; + const screenHeight = Dimensions.get("screen").height; + + return ( + + + + ); +} diff --git a/app/_layout.tsx b/app/_layout.tsx index fc8eb4cc..444ec7da 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -335,6 +335,13 @@ function Layout() { header: () => null, }} /> + n2i|OD{l2j;5QC?&0g;b_iR+4jO#)g@;In$<6MyuAddZJWx zKoKcINhgPqYDEsE^MQBieBk|ltKaqZd^}!SeSE*a&+l^i{pnTP?Qy$3&iBXtadC{TZ3ES&A@?)j)PR1Y4HlMHoWPH26Mpr z@Z!RY+pFj_;#JP@yfMR)$BlKy)HV6Dy=I{X{j%#BR+N)JKBv%eawg_vXOSyE$#G61 z*AS2bN_&H9)!3}!+~iR?lX_U*8B}Z9TI^0HDkz=nRfHO(w?qxv2C5AOV{(e~@^hU0 zy!^cE&{VK1eVve31gx{-WZ- zG2`+bXC+)M|E!5Av*j6v4=v2f&M9)7N8qaKOHgf!G&S-0IfWx~+B(iI@~LfkMad&Z z7YudII@6@zjE?C`7r`-E{n9@Ps54hG#%jO|Uf1{*S?4u(9BfhA2*ie^wL!)Hah6H_ z4yfvfo^7iA$?9KO9DR=CV1?2wP|etD^_VjRPT+kKUoSM@`!T*j$=$@O8~-$AMN3mp zth{Slnfm|7Krvuy&rH(GY|W{ZU}=x@9p^kS1gDD9xvd@NTrisKI4!_-Aick?v?yoH zFdV|ky1-P>2UNpza*~JV6&4j=X?Z~#6JJ!ERXnaJrzi`qh7Zdb8|1HR)78Ad+gs!E zlA2JpXBfZa&R;P!Dz)<*PE!Bg!TZFAMNEcR#=c%< zxM|W=9ZYN&P|fJ$9S%1wdAg%%d@;sjJWKOaO=GLNIu7kDr2|2kt1mSx#nA4KgIkvN z0<|J^2UXf5mpKmORO()C>dq<3&KjLHEazTy8RuqD^Q;h5`c(2^H_xw`RN_-ME?zpj zhZ)EMPzLORF2}qPl%rk)%D;Q|beu+DB}kV`SAsHhJBwSdG!A|a=~Z_lP<5`j%A_C3 z*k;qOUG$amy%x2SN^T*ICdF7#{b~zp>__B`DVj7U+ZkC@Fotn*mUMHR^U-%+W9(1~ zs_ZX%8K-+4)QWoQwT{yq{3g?+UxTi(Da;u$vWP+-gfm@BE5O>0Q&M_21MYv%og!?LgL!(y+x{1=wH0Qw3%SNrkHyv&<3X8v zuWiswi$g$JEX86yiwE+J)HY5I~nWnzNoT7r!<8z$bOUld=FcoAvl#T&U z1lha`CeWK#W|^tn1idc$0G#biunE`{{1c}-16)D{!aKnxU^`Id6jr1Y^8c>$E88d7s&19Gm^&2TU_Z>EKl` z+_@XBYVwK-hK|h1E*_g#m{rUfViDD=l=+~lodLQkwO|-~M_%FEa}2LP*W@1$DqSw9 zW)*T`D=K#4oJ+jC6WX}T9yBZOLT|wdO|#CVBqjbL!4#4|iW2eV{Jb%o!HS9>!e$!f zDHc0{8l;=ZCu8?*ZI-Itj~Kl$FTXHrIQeZkxn8rx2I&@Rzw}Nu8 zoWjC_!sMKZgZ-W)$;^vI+~cCONOd)sgX|Z=zJkDHwWDfD0*49jF2|PIFhO z<7`jQ+KBtW6NwmJBuf_;jbCkM^fYu?Aa?BU{erPTc3yJcnBfJ^DAFl@XwWYLYm7y+ z#!Sl3D$dUR1Fmu>@bsXb-?rA|FU}u3S{th~lM2-0BCs}i)3}^*Ip`z3P`#!}vBPur z=)7@xsPnyx>NP0o+S*i#_Y51IHzwyZGBzde>lU8?wOq{vwOr+cr-9d6e0_siH#@#! z4hJzSFR*%v#fw1=_s9a(?KqjQ1*39aX;%Uori&~#0V%t*g~e-LHRkT>{YY~MQ;a5V zXNz|9u{DlZT$r_RqcMRFL&Nh%=Zq~Z$kz#V;3iY*sL?s&FrAY^6*|kD1vHo;k2jTbZkrT2c+WGEV)Rg_CbH;_*K9%9qS z_MdkVPeNY~$~zanY0^Dx`OTorawVuS?E=;zRC-Aea7q(huYUb=OPqI1M`F*vW5pjMv1#~EgWd72>G(elw}brZ_{R54`Oks!_QyeazdiAGEOmC8hQ=Oh zV+M$MdFjD#}ADOT7fcg6Huo7d7s&t-v{N=>%h~&M?ksu zZ5Bs@@{vri5!exI0JZ_uliR3=@=8jJ#$^{~73DYypBbCwPo@ui9)~Moac<6-yy26a zxt9O_scBGbENam(Mz!>uFHARw6|gjocbu^WMa9`gMb4*IAConvAa9uCJWIvu(4zse z@=W~7%!(smQz8a{O~5XodNgUjX;|#>Gq8YE#}x zly}89=6Ki`Os;`&j~69?zHRu9pG-GnyR_YB&ntbI2+ih#Ura(41g>|Aa>BnFgO!4H ziQnl}ozkG>_TNl36HwK>=%oTbGeP-LTTsn<9#py)51Hn>@CNWlK~0q`%2Dm9 z|2BT}4Xu#>r2faG+W;!Q)qjI>OG*=tn0}1} z)BiNxAfAV{oQdyP{S{Da#&S^p@ubz4JE6dyv7PMz~=;0akKm#*09185%3zUVPMOVHKaFw$dE=#on72gzHmZ(+3AfsPrG!F?t4EmX3fK zD!8Rhcq*ut{uFN-u+QRlPzA39PXp(HYG^iNtBUf6T_TTm`g<2OY*5k%Rn=Wl&s6`< z>#t#1BZ3jl9?g{?xt9E72KKw8P%v*N);HZ6X>kv_%&{F*ZR5si`a8~RaG5Pu`+cUI zlG5029YK)&TAX4so&jn=#*X3Ho&x^4T^8r$kDY`*>r`W}J>bctkL^l|*P*<`Huygc z%(&Es%g!%046#I>=9DfaAi-XqlrpumlSdbfc-+fs)HEq}W%<_?WksU~C8H=rm5nIO z8k>95XlFvfm{D0YX3CFdTGVV33ikYdq^nQ59-un6;tVsGE#a!Ok;T6^3U3{kH@Y}) zOwm8vHYsbg_M47Q0oQNisc={`Q{Vtl*7^?(P{C)PW%Pq^d0lM#`Pj>9+@R~bs2cRp z*`Z*y`?a~TdCYdP&1<7gH}V`aNd|x_eW16tanq9XS{REq2IX+E8|J$3V5oU=8Jks9 z^v}nln#8NEg{@4!98j&9&wYgSe_mtNXhh0d#EHl2H>A*Jd zMDP03&o8;`5;KCBI_P>wxVpUYVx!~CBe)OX-jM%tbDX4zS(Wrusj&&cHFiI>Hzl`C zG5NoM%Ww98D(`JjEgox@cPG0;kp8UF*9pibYd|F|Dk=PoPTC4g64M_iLREXC4Q)@|SvZo3toNN-$@I%^|Y~+*`}=8tm&bQzE~F zFLnt97chTz4LN7R7s7Qa+TP7973uKP;dgzA6%BZq<&zYng8+rxFbm~m^Un|aMAUq;KN>J(-xr*ydRr3 zDCyqUn7ciwN`C+~-z&n#Pksf}*dO|tU5$GORQa!2OR6J>P&P|Tfg^Jbj z%E4x&mfmRE8X97^u^YX2&rFM(Gt{f!?A*AW!@Q2o&L~OAF;;E{YJnR&+%#w}sKO@Z znn^c*gt6Yepma^AEM}UvHWmYCaIVQu$CCAEJ~lURj7B4yo)of=mE@Vx>rH+cW@7&6 zwrs*B&Y2@kYr7Jm;cf%U@0x>Z(A%~ltt@V|4Sdn!N>CQxJKFf=+n^d)k#8#E%e>-( zQSF?Q$CwUqkC3bpFb%}Ik9`NuuV6V!-I|;dEs&nGe^>FcZ{-<0c@u+8GCzVZWVd44a*m zpEEjda!za>jUZmGHvp8uW8cEuHaSS|J<_~Yw<%Li**TyViQb?Vh08!qhX|-L8r^Jo zR^bSB(OFGfm2UsdUhT8fN(N641w9MVWA)U(#T55nP>b0QpqjAhR^zd&KxJP9s`&Y! zX3I>{soEBxPSjl~SC-5!7+oNDO}O2Z(;HO2!l#3oQBvBHgsQ0L9mW=Epep?5@_h>F zWHT33{Gc*}StE5qa-7|8O~K(@z9x?=%)1sYAL;_id5@Hu^lfLGhHe7S&?>u(fEw`p zou+4xS$qJL56l8pVRk{ms5}nGb4jm?@(cBCqT@Vpm#JVns6j5c+r;O=WzjS5G5JS> z%BMv}4gRk;~pP(8cUVi^@^K28Hw@whR=^0MphvJ!ayLHdjNo`8V7IQBM4>@AYL=$dv_pbC0yx#3$DdH-&iRx%F*$k#^Z1lLy1;FYHS zUqKaTzdCVx!&OYT=gl1W=viZcubwmQn!3su=!4av;LEI~U>nlU2AhGSKpC(**aB<{ zo};r<4FXyjM^b_HCT#lZfH7fWPC>^f+9YA@&Wv`nCoDV-yXQ#uMQZCTo4P)Y*iFs7-I0wRYoC_3!`*gdc(` zVD}bdk*96K;!5KqM;|WWDxh$y8Hzkm1rM<}EN2)82aXQ~>IS#`Pnk{cn{8&Lzx4^z z(KT;|N`i_zZ8u|eE~wd9&*GvtP0MG#W#Z+e$z1-odD}GLpO%Z=SjV1U%c)2mdh;FA zq1es-dboV3gY#V5))$=Fa9!i^51;i=lbb%S^<=lp;(mCf?j2LztM6&O>A4|S?XMI1 z(@XCgjk}+=2Xt`I}HyTRw+wx~ zGOcVMi+3l%(qg5%FIZMndEH-ODY2As$@RQhgVGYl;BU&{C9P`X|2ZvG+j9p;-GiuY zgQQjcBjHkf^J*_+a;p0lp>F7Le$w-ylmtAY3#^7$H6Y^lf~oj;PkCl4PI*FGyviG+ zaWzl!Y7a>ZUx(XY9u)N?Ar%!Lv@MPWMis_+Nx6~m#bQCK;e^!tntsJ|R0xyp;#6Vi zL$5L`>egrGs@gcOVo=2G2~#;VDJ$aM59?LxBE^s2~qGE2n8u#hSU_3+Y%MMD$4vSHDLpNZ*3jBMv^805N_vPG-D zP%u_^c2*f_jE@_L({x;et+wg@%tjLa%9vU#rah> ziT9EQM%*PZb*h#|-u)S-s)J6sUD$zDY^`9JC&R2Qheg8Az)Z8gC8TcF2+C`HrYVnM z?jH%|dTxF+w9ZSzgLO&C|VQ^*BKcJee9)=iH7T+MR|dVZXl!% zJ6=*=#GMao2dnEBjVO!X&WYF8MVRBHRs`3;eD2_am2a1qJ|P<3fIANgVumL>&W%CL zM$erXjca^?*K=Z;n}0#9(u(OR2@uuE+#eQkzk$h4A-|$eD5XXl;(~TgA!ORAW$Hzk znE_%y!(;{HP!TVEa?~AxtyC(r_@k5>kd9trZmRn+AsIeM8h6S?Ue76M?iCjqv&VUv z%(F?b)loyCjL@aTHukDUU`<$u zqpTR8?PqchiMZQg*T9&3q-@W8y56vXk6MjgFRCnM3?g3b>1pnmNUU>O zWFLlEXU&U*KZJFXz2X|Cc(rdy3-$KWZ;84KF*#kL&C0R^CR3Uo)TX0`Wilc+z>M`Z zKKH}y*pY9ijSGC=JqZsF#@nmHgRg`ION(D|E-nv~ty#jEgKJ>jV8JRH{x@v6moXtV zd`Bt^xI%H^Zr-dJX`vln<&0=}7vnI@E1!`XF2y$nD-`<3E4w`!clKr8*4xwERhKzV zZ&M!g#o4&vR!|z!hH4nJgw(nEA=_L6#?A#sFd; zz|;|%J~9%n%Qz14w$qkngoX#LIw@UW#i&*Jo;xcVTIQwCiiUqg?dFx=o*K&bDrd2a zv7lXR8i{$@UujmjU;sj;UfJwu_+vCSx$E!4I#(dDMO1#?rbhiIRu$xV(I8U4F%+A0P zR~`wC@!X22`y^U6DQc_KIJdVqt0FCY8H3p;=+c9pdv8=edv7$nn`w2s)bPwcR33yL zzCl{XBb{pW<%!QogvJD+6Z*x*Ww__wryVo>zGyhVzvJBECElAF8sJsJHw}oTzh
VVE#tvXH3$@| zx^Tm+n8Vyms30irAfZApV_~NnLlw}7R|TQOCpy)@F1-U+^9d0-BQ?$)=FOU$7M?PU zx59z~_7f@$+E|*Seq>P5cZ9|UEgL*M7J4fPsV`|GV*Oo8XmpUSVQwsR+tJVuK`7(V zPBrp0xa#O8LL*ddT=Gb-=Y+Iy`N+UynY!N->K-)qoFd10DMQ z!YT+oS;3TZCIrCMq!KQbIQerJOe<7Fv8Xbna9QvKq9IknyU9(_*2!g3xwiYv9Ml zUdH{MOrZRMSb(@8B^m^!dN&BEwC<&L%)+Y)nc+BhxPXYfU|H)1@ukQFoaYoZ8uh7kKH*qM;vy-|#Q!$Pah&(w9fwOYb#Xq2pC#M?%xR%H`4UD`>&$ z8+V@P^<0q_y3;FL5p}nCM|Vy3$(r|>Qw@)4SF#tv%=V&lTG`*&23Q*1GN;oce`Bfl z$96L1E3zzj&I+yaDpy9`185hL+dKd^dcbUI!7l4w2h%Achzmar>*j4Akm?>J)E>R2 zpIMW(`A4^l5s`2nEG0;_fRIvgWE~O-?eg4J(YOY4yq>FgVw+>CVdvlx<^ivARn*;W zwP1IPYc|*GxjM})ooiZv2M&nDy)@UWotqZt%=2o$kQPpx=Qyce#-dbr5}~%#L~(3i zyJ4nYojy){(DaWI?u)o-u#1C~a??T^$3qV%jPsMr@pvT zU@C=EDv!`LU>62w;*SY+@)K_fb>fff^jwz~y3s3J7Y%#!Ws=1FRCgO8J4@JwFL^k2 zMwv@!h?n?eC$I9wXt>iOTEschxW5w`6jZB5OnTH*%aM!R%f$426#kS@Pp`ZpHSYWc zUeEPu?jV-9pqG9Ur|1M&M+)J5%OPS1tQX8YL7%-ln8Xs}oIay0zNLhXI3_b{O@ zLA`p$8u}P_&4Hnp5b6{ZrNg@OxM4WU$P~|gB^oY8NeTA!HH73=JhoRvLi@eSSE6pl z6PzT_IFt^Fgy+Io{a;CS_Yt}jy@{v8WAc;LSa=$&v%h_1i0SlN)IDL585Q%4(-X#V zvb28i1a~XU6r-`-WaF9yC*VK5ve%<-$Hm4HHG}7_Vp!YYS^Y^uU4o_@Cd4VHG~Tbe zo{?QbZGpa!(B?>+1Qu1f~(ejX6N2ATlRw-C7CbP8A0xyn z#HsBkLc!xnxcxKo7%U!cv0SGXg-T)}cNZa<&+#kDLn)yZ-qy;r@SqhsRVm>nLcJ7< zJM&p@>(;bTvFC1!x@(>_PvZ$*)v`$F8?S6zG~8~b{85?82wkO+9-rQfx=GI&Be2F| zgmjpuZ4JNT91H}LiHR$Xgg=G#^0%jiIwd@BrnA{MM!<|`$|{Rs7m&w16zqgC5ve!4 zim9xSn?XoE$CAhT`kh5;(m#v3?we-S#VZ-D8@x)ir_nTmj5I6jm$05Nb0OJgyJ>MS zU$~jC{G5ddO>LnJ?yi3G(!Yo{y7nz|uj24&s9*Ne!CUY}3wIH*?a9GFFxvZIW*F&9 zT;kha&o9$LUA(d{xiWdj+xlgiJMkT}lmv4p{05A3&dAib)2h5#U!}QsRGCg-3{I~* zVO>mfFu^JB#-;D${}HyREDEnHP}RTwW4K^T zJ`*4LX`~z$cC~;3!)SyWuEn_Nj4u7Fzbh8)2F>EMT0gU-8^uqH(8x=Jos~ zEu8;ZaIeYfeK{e`6e{KrcNo?l#x}>6+~)IGoaRJ6OtX%Z>mu$8Fclu0f!rTq+NJOc zQnvlV93B{K9_Pk+Wxqv3PkEKUMZ@0_(@ndOoA#y2!Mar%2^D%}zehtadzHUO!*joi z9fVK++CET)rg~+EqTwU(ss6J+#Ge%U#QXG63%7hfE=#q+BaFKbCfl1Yhgu#ux*77- z(lD>`aMYcLW*k0qO2mB=b}8u_dP!9gx9-2p@zJmzFg@%tsw~AmOp8{cSM_cr{4T6V z@WeOno7i>0&xE+?DesidJVlwFi5y9OqAO7t^_;LtGHnzeOv#O-|0tbO(Tq(#KR zQi#Ly>^+-c#`Wc>XMYzP2d$t(VONuq=M9}CVHyWBJO2ffOJh~W?}qPV&RFqfBs>S! zC79p432BtsvPgB}4|d53&M98mpHcTNq)sF_A!z7cn6?DgGEPmM?oSUz-JgGAXwVpN z7U}EmuM9=Qi+*NfzkhouH7@jv-!l|S3r+OPLZN7ArC$leo%*X^J1&$KzWZ05v2+9f z2_fTr`l|1Y-%R6!`*`b^}#9X1D|YyHZ2s^0m#rlX2XK4jKq zyfizd1_XCopXzQVWOB(tLx*GLl|j40`UE9vZSrBgyz=Z+_iIAEjecXqz5d_kutCA( zdG;vNOuP0!u@b~~!c4vj%$xe(Sc4eKaKj_=9W}I=kZf!=&lP_$_KR;}noeZ*>s65<$^fU}t z8wdNVymhc(eEg~h$p>q%R%y(`7lcCQdx7vsLf3hTn^WEOgp4J1mOd#i#FoP$C42*+ zp21;Y0U?{>`$)J>dr&hFujH`0QGt;B*)`idzc1JSdOh3vYtaAl9LJ3AT&DIrnN6Ea59a?s5U#T=tAj~y_@VHSq&M_4x)r#9wsyV|jKSM7%r`>wU@DWv2wU)Wf=THoJ;YP#3BfS%;6IE|@1W;v2${a(?BPFP zJa&E3DIt;W1jWdMA3DlZ;de0O6JqU83^{{?lxpaGFk=+OeGAjl#2ps?(z9P6=GlJ+oFTg$Hqg(BfwpnbIjx_b$!Ub@Q{ z+wJ_y#<+L!Db(U69`2MtKy7EW@0TKy0&#7dCF?z)? z?SQqt%;gbxIZUPV1vWnLHB261+TWtFX)(@H-VaLqGFz-1;=F9ctqgLZ$@D3qK2EO{>M4Mo7~R17n%tr-y<|le%6~1)c|MM;s&e zd&GSQW=g2o7IAZ$@MoxkHmL25&oGw34>|q}hp9|sfpsvoi0|B&&_0-)G+3`gz5TN0 zY=aLrjTuP;@+nLPG<$LCnXw_zBr1ZL8LdrWElh)DzTE#ArUsfPy;jX)L(U2k9s>)$ zehoiG$b51A1tA$cxW;fVILjJNgApDHGlqLcAyVNGtQxhN$DCL$eHqNus4ZX$OvA}^ z+nbUAk&l^+ij&U{1&0lKjg^POR51-@*I($DwZsPd5S4-3tC5lTrsssb+1Z`l>F1a= z+DxxqFuNjg!_nki(-x-J`iMIKrrLw=S6vTgW=19#?>qgn^QfXri`c?hHGr2N%u>bL zNRvKdJDteaon zoC*AfjY#k-hB5l*nPdsUp7|I|p20~I^L=a?-)pkOoOV7o3SQ00BV-0Zp0*68hPr-L ze28}&EX4zO#%k5tyg7**z8(qn_0!wX>)R1!Z5}a)MdCNL4taGj?CHs-B_Thv4dwKK zsyAjTJ_HL!(ocGnM#E%Fvj{Z2Am%li(0yShWz|qp!qhnKin!6<12g%muveWnv9VJ( zsPwNTDD1(w%;p|q8zGs*ywg(aLbHG}M!AttcR#(I`h%!pFwJ@8Z|vVk88!@E?5AH$ zhD{flGR;;K*Y+r5ilDmUq8z>Ggu(8E12 z4U}nElZ%aI@Lc+Py=7)@mdE1EAnt~08gbs@O^FjP3GuRsZua{U(h;3m^Hxd?$OT?v zX{x(}kgUv1VX>*-K9)#+I0R~WYmL#T{z zVd|Axe>$hcW(I3hcnT~yjJX@E&U_o4k^s?3Gv2Rih#Pb;10QrYTnOV^HeOnLhmfYY zF+%;0reO0F-3xXRaeV*2o3}K~0&l!}`Ckc{CI?}rhnkb;q#9=KPX@s*4wm~0g(%L9 z#YULj{xI_T^3-4I;$7MU40cEgRdar@W?54ftuuEWd zg1Ni>uQ=vzoirL1xXV?9B@8d<@V6$XSQVU zVb=yFXhnJpW+tmVE8z-L7be8(2E$}f#t4sm$i|tr{{&MhhIQ|8^x#2#_rT17MYVqd zGi^zFHYFiFwyY@X4%m>Rg?tUuY@!Q{?Ik^9(?M=ja+GOtYI|iYrAjIN8+!|8I;6OS ztIWwKFjweCKfNcT@G@eQia1i_M?yz@{-_;a6J=N<1^#jiEI7jZRm(9Mtk2PwwZ1ym zYPItj*iglpH*2qn8A5RbVamr>1MCIil~D8AVvSz0$)xkdB`{rRa9_Z8spDZh0<+d$ zo@uhP;oO>{KXn((;NbMQ7bX*%?cls?jXn6%l_h^7%r1d!!Yg4a+}^=^je|P|W_AR5&E=!QaTp7qOmQ^cQsCE!N;g@TJUmXH#@ zl=joF=luR(M6OVHQMXfn(`p@WQW7A#^TAv=VI@rc#;|m#;ec58H1OBL)abyL;aOtA z0{ammlSd0y`?dezcr31Gj4kn4Q z)?t|OlBDdEgu!NxCi+#=alaBM-xwcCbw47cR7?Oor2dVu;gmD>f@xwixH#jBFtazD zGUVu_{yHUYNO1ZKFF?G}EAN}?I$6f?$%YqP1+&k}10&%FV0^{Qdj10;zFF*tZx1!q z;89aJxxqA1$jwx{qTz30X~DzndAY{& zJhAW=+sIsh>k#T*h^jft!JOOV&ta+?pJU}aE$`?Ni{<)Cm_{zn&wPyUYxDftL&?7z z)!y{*JbuZ@*y!q7KM!UWYE91)8`$i9fOX# zCyqAWi&ryg`&t$(I&t@n_O}kBDZir19eMQoZ%RVG@ho;%TCxDvIVfL){1HrNDl_EG z#u$4A$I5UvEG5`g6A0O5XnZ8R4QARBzPKP}e|I!NlSVy!2&Qui8`J(sc>7<;!edQ^ zj-T0%*^nw~2Joi8f{O@VqC2j*56Ako^XO3hn@l!yLg)e0*whP7<>69A28AT7Ca94y zUqa;8PJpXb!I?3xaiKqJWGF4JccH%(bSnzYIe}7nLFE7}M``_v=A8A16vc)`6LKl6 zzbS~xTC+G5yiv>-7oVhriv6CWDQz|4upkNE>NXx{%4T+B^+K3NoKfYX>J^w-Ew~|Y z&lqn;gmrg6_n9yqPH59Q-m#ycNvl`T<`N1fZMeZiZUFs^t~@r4Bxn|z%%@TkAgaSW z+Wi4Dw-9Q6&q>B&A;0QOc2Zb(QgWEW<2G4lJiYd0<9|FZt|K2zyBJ?SvwS}Q3*JX{ zKO>}PerCXEhVhgs9HIS;5KX#@pj|}R1sB4!SSRXi829TGzjh&(x_7Ec!*b8V#eUfJ zq`^|myB^cbZis_WvRe+-rAhD-YTS-#e$OHbXnFHdm1Uv;rVYqE4b6vL3FDdtM?3=4 z#H-;~@iktJ60PprZ%mEzO8l+GxWe8NzxFsvY*A_^e`0X8V|Z$U$qc5HPP2?#nn#CXm>S6j(Vrs|Onv88 zfor8hFgc=GH{7nXjhR@hKcGLbzna$ZPE$n?7v2c#=_lSxcgn4Syo$Lzr@(Z_hDDgI zvuvE%o3_Ht&aAjUVEWpe&f%?#?=o?mj|W8JKfH_6&VbHt%e#$3nnAo3rcPp z-)j<^+1neY=PiyZb0cwO_xiJLrNEz2HCSPDi;?P?t_Ncf9uMQyBp#%nQix>c0Qfab z7N<*WXU*=5bz2T}JxrSv69Lb-AC?JYk~8JMf@y=n54oguo8NCIR*-ZUOhe3mlN*U! zc)vgEb{zWi`~9t;d*cJKapZ-;@G2OuKHSb%T#oP0x`PCl`qr1s5?u&YgBcm#m3tC) zoxgoLi_l?$S~X4xD(y1I^q0AkHF` z<#HWNn-7<>eE|II5T+IgZ@_3@HW&d z{Oz-8YW;^|Us@#H5^+bsG*V_Fy#vz}z&%)nPMz=9E~m(%`Q~887rqR|Yp@$hgFjYs z)%dX4!Ksd!?ZGrmWT`FPARwU>5;X>iPSZZXWBtXMQQ!P*nYGSQl$hUr9S zW@V2jj2rO6250JzU;~5FJ3eWaQS(?e8m8UIyspEGH$hz|2aeQuk#Q>yTUb5|Chuc! zq*eFBv?HDz+=9$o9CC7lnl(Y3#YPQz>Toehk(f#^`Yw{xz(Pm%$f!r#=%aPc`>}Y0lI8 zb$q`ClXuk#o}jB>I-eQahnAZ-^SwtNOyg{PV;Rgow9&=`HV!{$$!WeKcJ|Z+y$+@c zYM#Yr!`i~IacLxO{R)5ULpbwKRy96!_Or1ZdZZi(GdZdl*Sla^%h^p}%~zUpt?`ak zFxkzVxSZ#X_LE+x!Q_c%CKtnW$7u2mcs>-oRbtp`m=>$xb=mL{*rk5PQueD=)`$Ge zXL!n3?e~0?NtO3Ph`$*m<53=Y4iL-^{#eOXYq<*j;H>`6$o$)sNg8wfL_bLvaR z!_AH08q0!tm(X~<@z0|#1`S2fcFhGJCfyFx1dcbiyiH#=Hf6jxHQi{L8K*~J#_y}9 zrzAj3gEMjHE*tD7?I%rO*O>v;K4{wt`>WrzUa6Kbd?W1Y;HhvKA$g|B^Do$6y`AxD z>`6hhXCF)*U>D=Ty4`CkK`-Y|CnV1^^KvarvxA+Pt^Np1Yizuq*^LHfzHYjSlj6V? zFeNo_KkkHSl2J@mN{u)0d)n72fqLwdScf1^kIl6}Xx#Mw@kV)p;SCkl7_Q&F8~(pBLnb`_E>7 z)-onqk1f?re?QFB&X|S2f|)<3eO_g?yzWexPVlDo_h9C+MyJANTdSv>4$~56oNzNN z*ckkz=`8-+s`sH2Op7(!BySPi3Tq9+!q{Xv%q}gHQ);}askEJozmbHFnj~)PoBph4 z>CkoC&F)*vuj$K`K?$Nw6Z?}(|coq(cRx*8mypu;j7-J zLw?31%y&W>cJqDcc9nKeO@S$&Nx2s$SK{oM6$zhFCGRO; zm+JN>q*aDgybiShCJzo?-;evf%HO&w)G;sH3Er9Gx4~K<&!SG4p8|g3`6=Y51wV87 z3Gp+};)9?*!kYXn;HM5hOZm~KI>!0st3&!*%^YWie(@2;@v~Bdk5I)wXYqN9t3Z9K zBll0v%YpiLRQ?TCKL+*;`tuqIl<;+%P$>R}<;OzBZ?y5kI6vowP}360Tdgb<-)4Dr zRC{+={a7eFyw8vF@3QHH;=B1#{9emHu=pW=(+EMwArSQIV~d|ja%^{a02O$`8KbN9hmpqmNMOzH|IpYeJ2)r2SyEA1(eQk&jS$f3f%*sJy@Pqfd2| zekf24#uNSzKXv@XwV?+7>1#s?B`PaH3I7kMJaugRu~7LFiC2;nE!MSIPa+?o$~np6 zsa6Ni3KB?Af<~a?8ruZIcz@$s>ljU~TpcBy$uG^9bHNk9wxAp&736=X6TfPJUBE;z zL!w0mueMyM@wv|OYG@pznxOp0N_bRP8NF>m|6foI?Q0t{$mSo!UqKV}WH5rX8!Zm8 z34{t}SzaAALfPoT92+lGaJc0{tyKAz{|^i%rcIzUV{F3esFoL6U8n*lfs!U$y*f&o zVs)YNtDf^gPUnF~ECcIG=QtFM4abJyQyrC&Jvqq0ei(fIH>jlSjKQZms=V!17h<^* zhrJ}|(fgo$bq^?K`N$-2J^}Tqj!O5r)rAUv!7mNPS5~i%N`JuWLIwZDuQ+Wx-`WVF z3i!_Q>Zq3ggs!Nc`K4KL*rpT4`N`|qHKhF4%0dN?SY92~9@VAl;`pWFYk~>jiB_*? zu|B9GP6Cy#0jT_qZT#t=?A0uedo#jLbABmd3s4Eq169EJmR|t!zjL9*wpPEya{j1U zke{oKAYEs$rhi2JBjlv>OT}Jg@fuKirp4<(Reyu!{XnH3X!&5vvna0M8TgyrT;yM9z=92MJrCx@%++QC0Jeu zRD7biKYv50O`hoKHoB>eZf5z}7F&R7O-qZdKz)P?wzk*?RCyO!YzNA7J1E}Ic*S~b zXQ+xx1NEtn%H9QCYt9wQ2KKa=VX+se{F$ISf4#+ipguyy_qSYlGJH%t(_if=Mi5R0 zRpC^NH-jqh7Lfm)8T$1PRKD5h%2#gVtD~fQ_@xF^SiBch`upO!718HD8*#sl@Iif! zh4KCqud;6{`XQUFI;x)eRu_stZ1E9L^)9gS)lvO?!lrxD##cv`v&hCTj%R;Y$DgqY ztD`DdA>Ge-&F=5d*p%_ct2SP!ef%vOzXMeIoi<*m;&)jtRQztstE2Qi z8en}0{0~%z_o1u6k8QkA89%XHsQAx7P4jYSe`` zzB;OLZNuCes)DXIL3LDx-O<&s%Rm*B4obhurWYzc!*XG~U%rK*e`rgnUMr%UYi(S0 zROP*u7Q7+IvyVK1Ee6=+LKQvG^6IDo9BSjUK@~IHrW4kOPqguqEKUYBptpkh90Sc7 ze2j?yQ-Y7OFvWKy_fQ#d$XVSg5E+!ql$}kJ^Mn74(?p)lumlN3RV& z18Q(qf->y$HoZ^{dl6Lnmq10ms$a*#5+YR4Mo<-R0af5uPz7uQRp7feey5G!W#e~S z+ykndeW1$u#Oj}dD(`bppX#XcOTHjN6?|h8R7WNF*6Kp>gP;oh-s-{(cqbf0dK#$B zMQzX5SuRuq`+#IEaRv}j!2@lA>L`7X)rIO|7AR>1zZ9P1wOpu*ia<4N zoaGZi<(mYm{ve=m3BP2C+d&m{7pR^*2&&>oKoztA)aO_zqdZBxDqdnyf3d&vuK<;Q zrN!rk4G~roP>Wsw^$F0=f771OUbN9Kfhy+}n@q^pyUur3{|QvoA%3a2!=T#phvk2Q zYF8Yc6ox^it7*Bb&hrr}{)AdsM-|pX5T0zYp~cfJo(bwBRDsPb7mA++s^e#aD!2ux z^3Jz}TVvqZ%|2UFVow8!yx{QfRqQ`HL(U%0eX*tRPfDlR+h%V)bL8 zD!RkQ3sul;%Z18+r{&dA<=<`N?=kTujsp0N^MH-;EzSXDk$ILs2Q7kRU<;mm5Y&SuHo;RiflxhpTIjEOJJce0_+Me8tD}-Vi>_i;*=(zA zI-%-cV{x60f6>MZmH#DBhTURyq4I6D9MleI1ZCJpL`^^E9VW5T?X>BHihkeXE>Q7% zY`jqR{uq?KJ_D8ha~m&|{*~p&LZ$!O#)E!*Rj83}RGe?DC{*w#ekt+KRu^i;IArz1 z7XNMItE2M$2VLbJQ98f8N=J|?d%qn_P~xhi@`RYU>Oc)m;D4a>TIfobVB?R4N>|6G zI|0Tigzw2>$?7x{oY=3hMKzo^}+We-Bv|d}SkqYRK21 zTKX-h9{mg|-LIfdr+P}#H9XhG=b88tM*&6TgEDoYO;BVKI6|3tywxXIT_}CB<<(IQ zn`(8T%AIbxQ1#6K^^ZL|ciV{Sr~)d`)uVfDe05a(edxjmY`jo>j>UPPDtg%RM?sah z0F;w1k}m&WViP zbzv(NyxR&w@jVvzT3s0Dui6!A;lH^nR4>?+KCy8^<@pp;qrR|Q|DLZZ`x?|oD1N~5 zW1$-VBk?Nd7f|K?YSRnVsl%?_EC~d{5s>SP-S+M@%=A+_SJUwS1=XH9q!a2NIMzof zUKdpT^(?Oss@zj8Hn#DnS>9B>q_rj)K?St22`;pFkxh6BsE<$`)xWr`PjyuOj#d{c zU1y73tS(f#ZkBhejW0>)j-Zxb2G#?ww+V#O`+&0aV5hP^R@kef~G7^be9=9eE5?LrV0od@8UA zRKbfueT2%e)bi@69xX>#zGp$@TWQm;w)z^&Ujp^1j!M7Y=p{iw6}^g}mQ~seLh&lg z|2L@gdu;mZs0#O5T`2wm7^k&tpN$Y|(fcfjU{-=k_@(7vgZdl`HQ|1+>3_88s-w#N z&6ab>;=h$%YuSGk0RL<8h{ZoZnK)ReEXo&ZvT!MVEl@lW)R5EzWyzCm{3%v%Xn7-x zjX{-rI#{BNO$q2D6hF)I>ZlB7qpRX`K=BqfU3HY+%IZS(`~r*ZtS*#ucdUc`RbVQD zGIj>V(`-Vac+_GSP-EN!R70->l`q5cUY1{Lu{Wp&^aEv?L7+ZD@vJ(se-JQV!H=~G zgsS)^i-k7BcpG0G^_X%Cx*9YCRDQeZ-2cj;*)Zglf+Q%d4ZZzk)7&)y4~z{&lOrVRfO(-D0^=)o!y~ zsB+%ai0VV&C@NyR<<(Imxf5L@v=>yxAA(B1&*Dd56Zn5@{1H$ep}x0F!undUn}N#T z`~>P(;2bNou-MAt`4*EcwgJ_%b`~R`K0-Al#d4vpk*}~^sD@uIvly%Z19n*>a&8x(%eB5@$OBIsf~h8nO%2M|cYOEvN>4 zXYmJ6>3#(D5$c}K<#9#@C+e}rqQ<zcepFr|KP~wAzr9r1Oir?ZAI+4d{OD61kH1xU{H;oP@A0=Po#Ss+Izw2G zv?Nu3n^G%=xV+@}Tb0Lp%TgAr{x+p7Ca&G$_*<12Lc5jxM=o^ytx98$<8M_Sf2)#t zp(%0vtxByAe0LwL69%=W9)GLS8szv}mB-(zJpNWCE5h-&Dv!TasVfjZdb?7a=kd2H z19Lcf%TlH}{#NDjw#0xECfR z#!t;EY4`2FKR&%dpB-z)P5a`9-wrmp?DW1LKYH1NlUrmace;A_H(jnuX*%VX#Rn?( zZeH@mw13Sy^7xMN172IZDxt?c8{gmCspUbYame|j?jK-SYn%Pln0rFnp2PJ<$NhZi zs^W{v6PE1Rb@A8U^M9QG?se1NUg3;#{96{yJGkPrMziysp({g6ZzyTnT{w>ywL zZpXPZDy}%K$}ca7Z+O+2@6AdpTT?vnxr&zt-1|l1v@b6I{m`68&c5`m6CDwRcl@?|G=2|8~h$);}iXi zg7_;#u0OW`;gEzQ66*NrV-XhLgs^NZLZW|ILf=A!K{p}P^_Sd)kWhq>Scs71_bWtL zBVoOSlYF-bA-5PIzX;(Jf31Xu;}Dt@BQ)^yiV?O**e;=w-)J1d#PJBz#vz>MS4wC( z0U>!jLKA<|c!b>&_DN{!x0--3b0R|d1cYY(9tkOv5IRppXzrIyMA$FkpoDY$4wDe( zOh%YL38965Kthix2pN+RTKRJ)BOH=&M8f%g`V@r4QxTR;K}hxwOXxcdVbD~BHvW>S z2njbMBu+!P$nQ4|VU2|K659Fh%?PCZ7T;i{l(6AJtNeM#4&nrRLB4N9P4t}Fj zgo)D;rj;V3`jrw|-hz-k9ig*7X*$Ag3Hu~O{Z_Xi%)Aw${1$|+{vHV_w;^=C72#69 z>{f*R5)MkZ%Mv8=3NNo zxAWZZTragc##jljmat=bWk1)-jm_);`v^krqX_wrAl&b-mC$ejLX$@kd_V6|ge?-bOPK37T7WQdA;Po;2oL&| z5?Vfnkh~CKzCURp!fpxsBs}7`dJJLa;|S%CAuRCsNJx1Cq4VPikNIVfBkY%OP{I>_ zhbIu`Jc%&>34}%d0SP@8A!Iy>u*9GHB*GyHM-|Z~5Oz!0Ct-u%>KTNY%Mr?-L3q{QBOzr4 zLg(cOulr@o5%xB4L}Kz7k>a za|p{;B5d~$OX&MN!l366-u9O~hmf!eA@O;HD!#$N*dk%Oggt(v7Z4_{L74Ue!Uuk(gqCX&lGh;Y^CzuA*ezk7gpd7J zYY}FyLnvR1@TtE?LduH>o!23J?w74Y*e~IrgfIOLFCxr&31R+=2w(dLB=lI1kns}2 z0e|jG2!|vbk?@V5z8+!m%LvQXBOLS(OX#}+VbIG6-}_5mMo4%CA#nr3kAA-m2x}y) zm+-UizJieZDnkA%2*3JkB{X~up~=Us&PRR}Ho0}^_?hmi3uLMwmny9kFQ9FcIopZ*@g z;vERf-a|B~6JgK}gf{+?9S8~UBP8xbxXACf6Jd>n^%C0o?)wP2yAblHs34mVm#7_`1{eGVy ztdX!@!gap;DMIdN2>G8PT<@=y(C~AFCZ8eP;OBjYutmal3H|&=pCe5C0%6+c2m|~| z2`#@wNd5w0kU!}QgxwPMNx0E(^(Dg0uMo<=M9A{@NJ#k_q4QS=*?!qq2>T@*l#t_h z_!?o(euVj7BaHA5Na%3@A!9#6oX?Dy#;ht$M?sZd&#{JC=xW`1_)L> zL`bkAL0cS(6?cle6iuLbaoM=LTbmXs4y8D4afjkg3#AlzpMB0;0{ub%@2&Oj$~QA- z&pvzh_{^Sj=CqT~Zb4Xj8^Wqv5IV?nCDgqGq2X-^on+Z<2#M}ONO}iC7y052gzuHG zRSDh1?=FNd??LEr7eWvDK?wo(A!N7*p_jC|m!OWLx9m{oD+#=h(?`18PjJz(Jg)kK z1oa(`m9pw#f?tx}@ZMxxbTQvlzFyXij<}etz6&iTK11E~Wr9?R=9INwGK`VAj}uH%MhQ;hn}~Qb zyh?DxmAw*g4$8WR2_meZlCKe*wK5NRC~d4oyQH)yLD>cv!zeo5pb#6e34Y?zqGaQDlnE)_TEa4D&$%*Sw&fCl$cDuzOwcs z-)pW~v)+zZ@+R{Qil>!Osmz4NC-d#)S9pQ-o_|b*IRE6nb)1ad7e5*Q>cH>%$rqCwn z@u~6a+Oc`Q=Iz_~T#h@G);EvC8L&I?$M??AsSkMN>Y{)S~mcDW{oMJf~;;_r`h+QGq7Vur^ zc8nB~;IYf8Us2ys$IQ4@MSaIQ9J}I*74vQ9l9EY$Ke5VuGl#=#?(u;}sxX<6!>X-` z_p#rhirncd_sjW)CoR^Wc&uyq?HZ**Hd;d7$p8(^n!t`brkd|jXR$?b#f$lNb=st# zKKH#ASZt9kO_Mocy+dDUj#-RrwHN9tl6|f8WNzECTX!v#x9`Z$Y3-^@5!{rL`Bj3m z!$KSQ#)UR;-AtlNO-x+3ojx8&Q;n4@U%E<3^DkZViZ!xG9GrzYLnGEVbvI#0(BDC5 z>Ln|DP9R%4p$R?9FKG;N0IYQqVX!C znF%^+*!AoSeT!dzrwmOG_|hXV^`{4}s%CUNKEvg*ipXc!&zc~8wb*;A=s9TmAeg?l zoWY2=YWValp-d9i77^AvvRqy_LHb^`(}=wRP4g!`xUU()Pmf~Ngy^gLtt3`Od}@O3 z84>!jdKE*{gIHDRj3Be2JutLP&~nN#6%mV9b$n!`XU0Dq=ugjZRS{XhNG0(5)6lZw zA0wIDA!3phz_TqTC_C4BG>-nf$G(1yzm1{k(XT2t2N)(@R77eM^qL?|cP@~|&>X0z z?72Y}LvtEh9%wCPhlSYiKS*3xQTaD-1t9U{<5b4{nKndqmtexvyu< zYK&pvj-e$sv~Xy54K0Zg5drO5fNyDiV^e+G;J5@4UMmzStmu{ z_;{dcUX%cXrMilE1WA7xO;9xc$A%VcXr-Y2X=r+cuI5>3@R!U}5wA>8HeAZpr!25e z5x~i5*vl!2m&UdI&pXceJll<1C# z$YO%RjP%L`|;b6MK3e}R7u`Km3k zjG@)Tzs}I~OkvH=`rtc>RS`dEG4Ru4g;jb(u-VY68WGw>=hTwqR}Gp5HwGbcOhqtg zTEE(cy(w3-Y?5`-um(2+-5w~I2w%@`8b-=$8nl|Q^ zhSm}P5NO(B_1t3>(Fu$d|1O9aXJY))1a%=u&kiYs+s4qk;xBLb+8SCnXceWYiqL}7 zUk4M^1AiTq=Brvh9SyA~{uzkXUnfKBg@2Z;QxS7bP!|*Q6+!b2t*fE+fwsWVx*1wu zXye4u6%iAubtSp%WrF(Sp9D>Ry$x*u{=9mC1;4MLX^H4@6ZxdNil{(PDK7gN_E`Mh zV-out_Ce76;VXlyXCZ5L4hAV@o{C6oq{o_|Ap`{&+8{$43N5{%4K}o4&^+=;Md;UZ z%5pix1dYJoNu%L66q;6g9m)KRh!KWw6ttBR-3<}m!!*A!L8A%!k-VsgI~tm%dkokJ ztrG57!#5WH5*e=|1{0*+$9NMo9{<;dHUXMyF#&uIO@EUO-$eW+<+6$>XSxz0NMoD~ zDj3={BVr1)L-_SK(})oKLnN#_BIcNM&o)8R2>Kqs_DXYbwP8;OKN#8q!#4xk=Mt+T zzCuCml@^;IZRmRw^N-&WLz|7?`$3PThBgOU139K5T54kXEjK~)@at!;wD($JX!G$a zU3;pP&@{md6q30IB6OOjzcq$^5q>=zzX9%AXd1&}pp#0aZ7_UG@E?*cDk9S4#kT~h z^ktwBH0=Q6jELp&Ow-AoFti_`X+dkJc+$|e;jb*!RYW;Rx~e#ht4i+xpW)YD>6~HTiNAuOoj0^y z(9Xy_6`{uy>+hlo+Jj$@F4o@b5;PUN7w9Rj+EeLq=$aS%Kw){LB8m_c%jNGTXg_|< z{Xw|b4ee+Anc*9Pd&AHU;181M-iXi-acMVm%LE-#KO(-ty$wwZ^)OJe+Lzond`Ivv zm+>lM1VP$IJTyVa@OzK5eq?CJp_L#ioowqoOiS(;a8@p>2>nuv_7OTA(=`63!G`wK z&`v-0{eItzwMtcn<} zS5XIFxJpQ4Bu;LHLa8A z@Z>i{ja|QXcmtYF^;H=M|Mb&|TDoC|=HyzlUrRUK&=PQc)f8uhq4`2PWM~Bp%>~Vw zm>c;Gj5I_yL_O$R%RkD{5<)v;Xu7!I()vc^Swqvsh0^@^cizwn8(Ly$7Yt1o93Lrcj#s$fkj=sDWxi;#1E)dZbwspl3vW4D^6MH6@({ zr$BG;6;K;eU!Zm)wG#~lF(4K+1!^E_4qAYgpcQBhz65PRThI=403G$z!A^Me3B*RA zG0?Lg)uL1d=-Q(sZZy!dCJO*PS#>BF28IJYtTH!HvyvWYnHA^>nHfMvpr)lXKo{@2 zSPudjKqe5Z-!;jMM~6~9c2kE+H7+@T8k7=Af6c`Q0fU#g4mSE3?L)O1Tq6PT4e(*P@pBaMNPd1eg{W^e(3TXI1lvWms4I4TgeIK-WzApxhUr9w^PV+M!{S1|!BD@e3BXBhC zIFKKID98kYL1v&cD4j9s)aWnp0=xt|fAP^S1~`GvE_62G2XyM7W515|I<}_-4CK=3 zYgd|qfn4Zlts}7xvO1^+105i19j$Am>Y|LqM#JS(x40|3)H@)_N*e{6A%GHKz8sk&~Fa-0vB+D zgdh>9$0Fkr%3lHc(OW(HO~1ms6YK)JfkVsR7i~i61N1A0&vDiADm zlJ(?>o?99TqCi2QMz2pnMbH3+8-hkaO?FK|8PJPj9E^;wLG7gSbeJp4z*#ieIbbfB z2h^0MrmKZu5m*eCfMs9>SP52v3E(;MberTh*oRKLz)ql_lWGJSgC<}gCKc;8o=@a{jf=S@G1CAmJMfUs*bk?P@`8khy*Rj zo0PbzfLgcIniUAtg5?2fn(7R?fF$Vr5zud-sCi1i6{;W9nFFejS5<-9rfvd#m3kN0 z4fcS&dd|Q;Jo*KjIG|s|Sq=1qI!nM(umI=>Qv2f$1yw-}pjN1wpcc>%c~t_HK{Zev z)BrkV(#I-k*;+9OYtrPYK~b0)=I6Bu1o`UJ~#+`4yu7`6z65!Du2kuU=3+w@E&e{ib>7*;Ajo=>8skctGTTvux-f93Ef<_7W4p(D5 z&43!WS_A#CZ7ki$U@!y>1;fE;z$V!!mV=Y*9{d0{fvsR0*ba7ponRN(4Zfw*vMfMq*4YfUfURIDxI_Zg!nP0S z7OZZ$79injGK&IfxlRv)Kz5*ZuzVmCgn@8S7^p3*DA1*#uKa!nTj|yG4aeI$kGq5C zF1QEo1N|yS9Oyyj>9<0hS#6`&v9JxEoY2zG;kd-%5l`~WtCIH2dPZvcODb2nO3chClO z08K$Fu!Op)Ru?sPRiij+fXbjSsVWAFYwCc0+9(s~Mp#nh>8FyCg1IPiTf2u<2v`Qj zg5lr`P!H4xwLu-AMyHxYs-NRM3rYk15OrY?1h!EGiE)zvH5Xk)IsLX@U2qKlaqtWH z6`Ta8fPPBhfPU)YAf7|uFi;zU+8fjWqBf=uU;&X018PL70rVr+y9qx4)P$fWgVXSx z0cXKEa2}||NMDa=Mao-)wxAv0X$qgo&|f2GkWLz^gR~IS0bSU>25JJ(WxIZoWe3;^ zc7fesFW3hXqVQ{6eKO%0?ql#hP;1U9FqNEmPFCy3iw=V$;21a#^vg=$fPrK9Hx^7n z3H?&-R?7WHpl9vB;JOrHWk7cDF}RE1d!R2_+aC-7CrHTyl+8x=_vTu^o_HOl^lKBA zr3M}Kv%Fn^8c+1A)lqTI1pxbIaKs_Q; zTcbC^c|5i3t_giV8Yb6s^6f}&k-I1;|VX$ zbuayF+h9^*eFqLjl9NUHfrDW%+#~B28cfHX1XL;gbVMak8IB6E{Sf390$4u5(l>Er)COKQ+J@{POuB?23vq0 z=~oN%LCDY4aNXco2!;Z+svbs(qu^(dkQ~$NBZj6a*6()fc0?QScI!}u$`Pjf5d(Dt zLJ3}B0r=JMt%hzjY^$;KHx&2<{0fc({hHo7umL;;_rOu`E7$=Rfn}gGNQ`{__=YO1 z#rJlxWY7;&R3e3EiAc8s-U;Mp{T$)1q*fKkfM21?>bE2Bg9kuO{u;gli~^NG8F)Sg zYSBLhRK_0S*$ar&dLZNr{GEL%|E_4LN9;}pQ-GTC)pW1+=L;yN=KtjIsH>4{>+oiw z($aGs1T@Q208OPD*AG*})xdrRr~&;PP@}mT$yK)INQQy@(;Uc%JDmX4POtyK|0hrb z_%rYr=$B$OVukWwA#$(MY80=Fl6rqCpu(@AfMwgJ?p)*EM0yYIMWFr1d{BG=|5O_l zsCZ7G8o&La>$QfRN01u7CxS^}+;lYLm-?t0zOxLB!`}ze5$^d4S1rL0@#~UTKiICF z)^%_V=mUrPk@v<_dY$5506JUJS(DD9PJv~_p;Q0<;8$=QQ~|mK(51iu@Dqqa#svLb z)jlq?Gui`ogNX$Eh^yupUC3<4-2}$t{{goz_#WuE&vR@UoyUJK|X=5tU3JcOYdZdDKnxb(rXdP(gq(*d=X>SE>r z?w>%doT<3h9#u<0=L%Z?bdsda+zoVYsn!&odHoi5eyXcmwpu#Zd~JTJoR-m4N^b=4 zUKhvz5=r*Tk*Q_fR#(A+=^<(UdnIf6 zR0f$qagY&cR(WtU09~ft<~jmbwXpbbgSjpS${>0q*Pq~z0+FBqQ2ua`8R&gsxLDQ) zTI;<0Tc-@4uKd|sLTUk;gJwWSg~qrIK?6`9)RSA=T$yq;!rPbR zHpOiMwDeB`rM>NMC1KlLnH*iE`gWHmurr*UKu6F5&QhQQ{`N9tyDP%43tp`d-DSsi zSB`y_<23i4435bQAlA^eetQKEhxT6)=Yw|yJR!IvaYq4F zcnt1nUnU@`t(M(5d9(vV%T(x-fPjfWmDX!@r+~?zD0(UF1h-Dd4dGh9IkFc28n7DZ z^?cl!Utr?LceMHlTFHPW%CA@X1?zN>`efZx=k8b(&?Gb!rNjB;$9H)7`jKc5UfDgZ-c{ zRpl!%9}!2mJ`B`729APXfEL&Qa`!jm*V>>3*aTS>aZkKO_tp+?l}trgYC!GDX!X2j zxKIISjr$X>R*#Dkz1tO0`3jeMuj=p!y)3;Dc zZV1Fp2hsv%OpU7>F)4r+S#n%m#C-&k02fFA{wBjGz#U{FxBl7T&);+rPw}KrebK2j4T^f`Enhwomtz23$H0Yz|sxd9|Wskq$pI_^Y^l#9kX$ld9TjrdE-8KfBVE{de`O51Us5 zmE?`F9&}xJWq{V4m^8Om0_2m#?}&8rAtgL@T;=TaWy-e;d=8;ZI+r|ZCsjfs!3Dax3$hATMxLh@^0$o z0}L;ks~tcaptV?))aT2zR<`H59cT-b=7olJgr;rD%l|7hm7)B) z=Yy_~xyvW+!9iDPzp&IkJ~7FM_4BJTF7I9^vDm-u3^*P_Hl^=5JIl-rU7U_a;UN(r zp~NQnPq+d-H(^KuLxsQgw|6~QP}Av%AUvGApGfl)uGYL1eEzXJ(9@H3W)OV7V>ZVf zOn3HkOHgP?7_&xseZm#%^iL#%PP%$KLlVh9CtVLYz~J5~SD@pXWKQZ13`*)pU(M|` zt}bpjso9#^PKO#Hf}y1KlO_*b0nT7Q=^X72s2T~ID)f3_jUPKLt2)E!$ZaTU_|uR| z{uRe8ecj0y6(U1IH7ykh!DbP2|IC={YvW#hPY7w$kUFyUG+MNfM^)UZ5=MnYlC&=J zNNEFP?C&IO4ELts-rYOS-HWc;b~g8hh7<}7iSU^%CC-pTYhdt);hSaIa<1ADSioWk z3keJL*)ER?xC0%>d7B)*-{DII-$%=j9xdWK^Glmg^YSm*0fRGXV(FYQVQP7J#^vi# zyDfD*X8f0TBx8?Em*5VI3JD`Yg(Ue|S72lf7_{X4c3$qbd-2n(PDfZoNK{B+DzgdM zcjkJ*ni1>oIvt@Q1q*}}D##9LLvHmff*~WfW}4b8Vs5H%!tIKYlMu2gCg-CI54xp$ z7)MBy9TF}bE~0iZ7__$iSfkA@=c`}HL@TBU+MvvX!BJB-D_;}%FoDJ_nlk&YE4WFt z6-AiVy$L0JeH0g2)UHi$lOUN50ot}}`k1vsRRi)`A zYSwJYaoH8?S)9(+)o0L;OMfcYb+4t06~$V)c#~T<$@$BuvIjnG1`BH~Tt4kUz2;U- z)F}Ej@xKD!MQMy9d!M+|X%^d;PP+xB@eRY+A``E;A~-r>-xXIZr<8nl)fM5)>yZIh zT>+jVR7bKuW`4wpDsvV+FNk=Q&@SMpth@y+T^3FI@v{rzYUKmw@JUuCuuMzPTiN*2cr1omD)oOD0_21)~KD8u7XiRhD$Td>> zI}ECdZ{6^khuq8ez@S|L3DKWV38`nuob2{*i} z;{32SR`I9JNV1z|w`=R&7WHbK|73d^?k;3k=k~JW1_^8~XVtNqM_$Q%(^ZrchZ<_- zF?Gn+pUSvGiPt5pEEUiWiyWTQ*3Iwr_&H6wUv?l}OTHkc0zx#Wq8pbe@4I5ggZSjG zmFUmhse`S&NlRfWS3Sqfv*n)6q0gWTuqtprX?lz3s!CrRPkT~99gbPqDQsi=`^~j4 zHqEuFtWk#9beSsXSl#7EKl@|O?{tdDq5jY)uij#``ma8R`VnsTIkB84;J ziOy^1Uaie7jb!A>hce(@xO_$~9F0+(8O*WUO$sWDqs^SdF zC&71VB{?uOXbt&nU!7!GUFotRD~w?WSrM}NE;rd@k;L3}xgB;7$jK!0G{j~Ia$};< z+P`t>l|IP}uKvU67+`WCc}U#DyRIOI(;Xto?o)}9hX@4ir=#y9ptjUkr>XcaLaj0P zT~&1WzE8#OB>oRvl}%F*pm=vwwZ_9Jh9|qoW#t1`dS~tkDOt)LzyU0mA0UZ0Ep>#e zE3wHF20DWZNa#b7SeFdfxxm!Lx$ix@-s%oRG$o+Fy%OCV{(92?A&t?TuhX^J4g9?%16XtZJ@Smf-bMH<$b0dXV&~8qjNC_As>%$NXL)P9`qW!mtcD06~ zFCm!;={KoRmxLXrU5*c#DWg^PRv2{Dz5Kf6?4dvQnIF$^QFg%Kr29Sh2icsp7!y#H zG+}y;Ec;Kav>0jCEN~?GpD58523kx^p@d_8m>8KQHeS+L6H;!|nrDTc>^NkFglj@K zN<(h-oHh*QXN;~<^~L#N@wdK`@h~{E6_=HNqC|z_tTTw?(`I7>{%HJbdy6qlwdf*Y z{_fO8tlXPwxK=;;V&b{tOX}J2h10`sBZNL8rt!D03eLECI8A&+HzYPY9fYM-axAyX zmGxqsI`zABDiP0BP?|oaTpCODkKL(d>{FM^Gt4l59Gtdbrhc`~$1|^yH82OAf`J+t zb9zHq=*-EJevM~vmXvBKO_Ap4=FdQ-T4=lElE~hwKpLrmfqMJcW7K6 zDc`#p#&tPIj#+b=@U(=YbEE;%1qR0i8Kuq~S@gFnBE+wp-4S#f z@#(9Li?a12cJ0)Oy-XQfP^YO!k`EYONm4jcl@s?1SFkrtoh14Nf=0>fmrQH^PU{X} zsbuF$fZ>syFI)wjCa2`mOP8SO&@8Z z-TM#y=7%GQkl0t~SV8imqi1~udxUH9S@R@WM$|e(Vwu-yV)x6smlR3?hM4|v+BfL6 z6%hDN44$=>?8QO$J&Qlj;c2T&p@JddS_ZaKhC9HUR;zfdgr$*YuUr`!YT~s@=Uc#J znq7VDdSjKgXMa_@2$J2q8{+3*EeR1ZWT?Ks$v|Isgf}9N25R>jt!K!P*UY=UhZY^) z7hZhd*-~1ab@b)FUDfRFF++pc8drRJ>QHM2w`+0_2|;(N+s(0X>Vwq^HR!)Eo*`|G z$ywat-k7X*kw!}TxqY2kYfT`fNfg8y~- ze@WIkr=@kMi-gdVge0(2lE59|c~H;JtYK-cz7~U4d3KiR&@?r{4y|r}tmw`c5Ze(w5p) zfVtmWdb4&};U=OguK0+?%H_1gXYMywZyc*|?B?rCQD26;twdXQm<~m!=T^Uy#-%gO zw-U?&gX4*f6C&pXy3t2({H&<3PBGHM^Pd7*5K~JxKX)c?Ccd5YdUDwh>4x-n-Los5SG7iRATjmmty|?)Vk^7iBRAc=x0GPE z5@2P6M)9t;=E^Eo+r%JuKXMl^CKvB4D9C#h*wHDUAAa=X)lw8SZIQmrj2Qv%PLVFi zk?i#KTauz%gpAcrKFsdq13mTH*#pJ!9$T_IKTX)d>4=7c!8XQd+DS?zV?g-^Y6hs= zXJvZbuHrz`{F&aFA%f9tG<=b-i`s^Y$fje5G&?afAifs9cLHV=U41#rt!7B#K+ojK ziSQ;7>YQrF)r@mjSM2*+ydbNRuF@aKslSvb%xjtk zdre-Ne(n#!S@Oi+dQqCfz+s?$ae{n1*zLN`z45~ne7=qNG%41?CxEYMS^qdkR-Ets!lqwuv+@r+I65RyE@ zVaP>{qYoteeuL}$Pw@=v#F3I@pAipEP=Zc2-;ZCl+PGnP=~MB1_UtVnGA|rDzuR0t zdcuxz3tre9nlZIa$f^rJ_;Pvz>qBp{V`LV>-#^*23MZB$=}=<1RBhr;GbNSVH|Qk7 zSc=7DzUFuHdV%I|DeUSj!KvH<10y=y9G42_8CfB&`N;U_ni7(qdv84dr&Rh@#mt0Q z2j~riuo8*+A@}4A55AwY-su|SZIHt=ebxz5m)K*VO#}t-;M+n}YxyE0c z(zXUv?bNI;&K_Q6Yy!8-J3w3Aoh`=*G$Z%B0srn7-!Q3M@7N;O(tW6_{Xnm3X=P8k z<5jok!S=xOM)gK=FVLIpe=GCvEOo~%;^(|=Iv&UDj>SsVB7EZc{(NE?D6a7%x983Xhmj`-ft0|vkroi9L?ssN<_5kum z+qdiAd#a>05C&p5AQfq4TNZbkH>t1|LV*r5YYDeHP`JvzVX(xdelv6Ilhs|uui6-U z`gKkAWp&3!4jF4(d>WoS(`jZI2Znf(Xc=y25t5j;H!+v|`osBRlbnt*P?AHjOr?P` zB^&8nBI~k|be`hK!6Nx$Hn+=@y|lgLN?y9$+2qCU6|x#FxfT|+RbDIeK^awx#(f@f zDG~Qs&kY-vtQx4h76mnT!y+C?WhMe2FaT)wTMPI%OY=3^iS_*};E0eyoR+223XZAG zl+P+8tIpXAM$FRKJVsOdmgeRSA~C(4+d z*lPnO%F>+7R(r|WoMg`+bHlZ}o6>I&etC==)QlC%rl=&(CA(q?~4#>KEJYyJlXvmlH9%2*$$tr5ySd{d&sF-0sTQZ<;CjSXy`&E$^0qSwUO*9452# zxr4l|*@XO`Gdg<)CZB}3XBu;=cjoCBC)+xqlCgVEm+1UlEt903$+sWo*hM<>`r79W z|8B!1$h(PkN}`n^H^W&HN@9HedZR!7wpd}!5!<9)Q+DMiZC}cZP`Z>9q3%Jf2_Mb$ zTJq}2s!$fDUQ4FU=BYWCr&bU*X5RW*FZ&;kDOQ!O^l#nRa~VSsHt4wPyUH2Z$iE`n$~FSetd z)w^%2y8ZiAiH~-Z%!zm_D=edKswoBB?*F*w9h0xa7m!bx4iJMVZ zEn#ZJAiIW0)xxB6qRdjqB-eI4?U&o8gbUxTNZ!20qPeQ3ZBXegB_b)%ucaG~=V$nM z+9IYw=W=%|tS)f^J`5x(y{qhqqzx37COIRZ>@hUr}yMHx1 z-#AsG0+xQ(%6*bFmRprkJTKWlJPQQj$jCMQbnusQyxX5WlcHe$NrJGBQ+c7!=zg&cWY

dOWio=R`X+QqsnWEp7-?9VdimbH;ZU88VyJVT?v;Rj6SkEk0!@BEM>zBLP`gBV11srBa$KGYt?eYKEBW1mu zhfOzz;elmw>^|j9MLk%Se6$lN1v} zZU1ZbJRp6ED(6zgKLavQI>6_d2ZP!@swAG=FH6i+CXU{RM0UwM6=d&Fnhi#|U!Hs{ zB4a;$6NP!gg9=*bQ}+?CKGxo??*46?!t{j2VbvXW_U#|rC40c4hkuYcVIkIz+Pl(h zwVYX#SdWi*KFwn`Vg?fWY#`FQoL2TLk@<1@Rqxfwro$lo?_>OYsG>oq18HcoXe zW$%+lX4q+u(W86rDPOkmNL?Wpwx4FoVL~2P$tD{mNXysU8YVBuVox*-DUjVg)tw&U zcj_jCK_48UP^(M+iqw$z%C-iL3_GPoMRzMoNuOsiWh1XDzUl3Q)v)a|e~;ar|8uf} zvuw@7!-)a;8nRw0SE3a(kl0FOd>@%n$(<+buzj}et82@q&AT(t7vVQTA1B<{@py9NcGAH>mnuo!ue0uee%dRVW8*KPj(-%;J2T0K3RV}Kl+7{ zKE|5@lDP^(220T@46pXW!o=ro-`Wg4>3+7$=lsH2A1!K;YbFwO#HHF=d)hjJB&kYX zXMs?ce)K9YdV%jN>yi3 zFix6QN5Q$$7ss=Xd;Cerh~~eHo0aOwbn9^gM(8M?owBAnLVtxpXO*#g4&JZi`E#Aq zp;Oc_RJtiIk>KRfm=t^50TNn+L7}Cztijy8ogAt`gC}7wgFkNM~c-V_ZmwVmWkfg zqMiqtg7j|dlH=^%ffZ5{B?X|_I7+s`{{Ldv%zEM@t@ftFB)Ya$8S2+2qO&rVE6?x~ zb`yUVoA_vn6w&(FmD;LF)dbn4e8c6LI<+KE9oXlcvL{1jBC{@S@Apv(9m1_P=d(r{ z*P(2^A z8Y6H4OTgV*g{W zf9cib$d~a9b4&OB0ekCWvjcgGtm{f9cGAO@40VuH^Z^ZZ*~U zvV=886MN*hA2CTsz9zmdF^$~;Z607~(IQCGI>m)*xhuq3O($IKC>_`ZNl(Z(zBOw9 zo$tHTT8HiDIZIx*Gq=EZi>6%4l%lKCkpT)*nV9;xns;6Qy!73gFUw#8A3()tL=NpO zjwYyKHpfDnu+Vs~Rh-?}un5cA?KR+eULrITLRXy*KE888A6=mtOYj?e9R)tHuj5}@n4)rS%HOJ{drdA+6#4)d_yLHri zEhv`pE8{J@FNi*Q_{Qtun`sDE2S;9ex7%wpb7%Ct_vtdLAGC&&R5H7n+s$IxdeY2Z z183H;Kh!6hp-Z1P>!0$I@bBW<|6)a8EoA?{KJjF1hQ^@PSH|)fRAkNY;j0MV=^1H>lj-;&P5ecxWwtWR)oX@-yLPB2L#zXR{f`j+nSp!5&yQ9QP2!c&`~ z=BT+qSMQ8F*&j@4MQ5Rh9cZ~aPj-os=y2ViGUG}giD*Ti8jb{In7U+D-x@vBUoZ?v zC@!%uc%S0c$L->dv~nj&@J3@Zhw3WH+fqs2HrrUXus~f6G-`_yAD)i)mGOG-5Ltoa z8T-htBWYK4OgcPEn`!96aHNw0>rvvs$oJWG80cV_W^S!@`f{F>nJV_NZndhWJyv=a zz@YQdnA>OieB0!RF3@Q-v@@U8(v-n8>cd_658SF*bbZOXxc-?p209&)wpGyEhez&# zLwlixBUAS)m-?0(XiYA!VNBDId=;Ay?S5`gbxSWx0cZhTFifi?gcAHM3_$otn3;xy2O@) zpU&EOixBM;87C7zmc#AYZ1jHbsDDV=*|{nnv{uVu)>xbKv3>8-PA4`W>yk9sNM%eZ zD6elY=`h2P<5Q{1gyuh?9^5cF4pBHDv$&&s=BlBpy4GfDEFA>w85rGal-aKwmFH&?f^=5tyVe14P;@cqX) zX*~fN$Z!*VidOdCN=4n^p+G-**wH=x{|{4Wq>p6^t;}PcV>>f3Z!YIM)3|t)vkNn= zJAc@t#`v1Q^p1}F)$Y5j$++uJY0`z^uBmkCN7{Qy^;qtXk;`4U`c1}vO#+5KwX1fL z(|=5^`(2VQ6O`Xv03C* zSGu9C(iaBr-A*HoPlS!``G#ygYW|K0IoyqVE!|Sf%Wm#WZ*H{Nq*`~v-?_ybg`p}( z8Y$5QOL$Sa*B#}0$+12}ne>IE_?q-&d?BCoAU(DaXRa60Sucx9Yz)lr=G9X19{_Qk z~qj{=5f*#c<9G^dF5?cLBu74y& zFCw>9^bGsQ9)=oKx%Bz1>N%rLVMI|=twQt6hQXh!zRw=#+tGM2_10_ZTqEOqq1#qj zq0X#M4gzUGZ>$1HT z>Q%$Bb?zK5chW>f#juq%^})0FmA!ho-E_69Sog*H#D!KM+T0qBfk&6^QW3{<)bOSL z^_$W^E}fL$%l>N=tGKZHmTBw%3x=WWE{@B z45Y`#vQ6#k><{PLsv#^*;b2^)&$F*2_&&Knul)f=L+Xr+4Cmr?5d zMXw#fGij;~Wx5oj4SLoma9FeChcO4L<^K3N3rVZ9Cs*H8JguyrL-3^__kUhGEcq!{ zd9|$Tpg_NJUbZ19=rIi1gPj??BOvYJM?70;F_7)v_G;>PLSH#I*c}j*)|U^faYM3_ zQ*L|ueqNUwtRA0-HGHK(f70;b+Gp2WPeDYaMMQGn*;g8e2k8qusuY=CS$6fOSnM|M zFs;TeL@6_XLaHN;wIQ{TZ6k1o%B*2H-DHg<$y+&orZaP|IrV!dKuw?fY?+JIKf z)9EB6WlXDU@meer$1t8!ityPW{b{eBAJJFG&!|d$@`TKJz+#7?%$ndFkagOiFUtX( zH`VO1yyh}0A?LU01if>~ek-;QEUFeFO(yL<6CpZ55BT^-<_qJdXN?ary@;nU96FIq z9UWP!$Ic^X4F~0B`j=`lgKj3M8GJg*KKv+M+b@s*txpJ>tn_x;b{+hWN@{1XY1~v& zt8+I!mFfBH4$EnaOv@vrhU?vK&F+%D)!cMdMvz_i-}IfPGMTh&F`q~9S}@EVX0$TGVIKg z0YlyCJ?A)2NN0XY)BaWO;WwAOEfhnONyR&Zg2Sr3Z$>F8FpO4XYvYSH@A8`VWBPon z{|xkw&+*;ioBOSP(;BBui+fjrw~3~ceVf_txr{Dv<;(~Bya-#{)G}uTgRsdQdN{lB zd&ZhxBw)x-(|)YG>}q_~k58`6Dj$zzJgO)4N77cyOOKK4_+$x|c#VE@#vpo_{4j=I)3$yFTdRVR z97d`A-SIlVUAg#p920Zzm~dJ$k3|pjimh$gP3udK)<7POMc6_~GLBv;@nd`b{z=|J z>z}62ua8&g#Gb+2J8^#@UyNg$X=Q-jebvjcBPw=W&EqhdJ(A6`14+!LF3x5_`o}nG z@jI47DK(xQki{~7JY4qnf@c#wrY^6XeUg4RGE0gcNHeb;?2(J(N#qZbY64C%DX&gn z={vz)7E8~L3GU2JcLupO!JXTgBZH)vNFJ}3Toc`y5nXYjyY>4N)fGRzz-Bo#5n)Be zF^QFI{mk~aT2D7mwsP>{W*g(Pu7y;cMAl@>Vt-Th$)E~DM-5r`b-a$=tSKb3U?jD1 zlUShtDPh>`osMi$bFw?!GmJW^wXW>0=FJb~c+xdK!>j^S%c0sB<=4sXqn-v7qUOig zvW;q;T=r-z%!O=IiM0{KOGnx5?9Z4i&C@D*va7*Gi;1qKEg?GojA-l{+pyf4=7gB) zZz;rMd75_H!Y3t#2M*jh?38a$pWVH=ag$5jp;iF6j>uSHI7|+MYBMB7gZAyecBrwR z37#59(i5VSl)#iJ+t+C>-X2+eZSmUcM>MwR|wPWq2j%!FDL?omP(5+cI}*E6jdyY09eP-#I_<=Uu6ZmFBC{-s{qz zTeWU$6z{P~@lcRX%v8ZQpIr?y*S}Y6a_xpi(LrCR zMu$}Sq{?)6C1)monK_-wpF6}Z{%Xemqed!sV}AWPxGAk zbudPTW%<#SEsswn&O3 z+G0i!+s`|h4sMr`Yu)K3W{Eo`w>Dnlj_~u|dPrt@!`~JDhQn6y!xBt7cFCbt==n}^ zGHG(omv&fhIrOR3D&hFX{@>VkBPx;4Q^~xHr9`?i^2stfzKAmRI`8Z2DS{^_^rupp zMX|||$SN@CT54tajb(1;NWwGa`U1R4YGFc7F3WUvYk?Ji+qcpm`1Fyj#No}w3Gzs< z>_nTqajcdS%OUZ$ke0e*Cf%+1t&C4>6jVM40^iPd(qt ziZ|)Ab&qduZvv`{Y+xEWqmsQwTUIs0v6&N&Z?S&_4NiSk8x~mq|!$Ef_Hk2 zHv-?MuNCjR{QoY$cl#|P#7b>?N%-BrThN=>{;!Vc?aFKSgYR~L|4}~^c{?*1w});n z=lAZTkso+KfL)c1oZzp|D)kO(bM*4?MvvqB@~x8Z2Qtv!)zwv-_hr?6(w6|wNm{Xv zdb5XaF1_?+vc=q~qdMKpF)S29&D7RqwpTsXrP3yPp!{;|Hy)$!yUCq_ zedcMK$ncGFXcGg96~Y6}o3Q}HU2QYm{iMxihOy;xXR~|8dlE3fO#7C|ye(D&Hg7@T zb9unk`=1uGmVN))F^-7eERJKdILhJO$Sp4Z+YlQidDJN*Ngp#7HQL5%=Ao=olG*UJ z-=%gukQYj{Uyj$K1@eVjFT7YMVdM)<(CIxdH(6ZP%jW6W>MJk(x3eC-A#=_l=%Kvc zPU^Qw><;DuuKF@}CwA_@o$d_Ixsyd|?pI_+Y4x4)6ydtTr1W$La^qxGqJt?dly zC1ooq@jjoYbxmugB;M_=5;dQLxpdAr_EE!t^HpBkhCpj?WF;XwB}^asS=aA&l+`tn znN4CoAG6yXW1jbE<(%3~dhc-`b{=RhpYC#I= zZkH3e_mBLG*146q_q$`F*0-|zl#dcFo!_Xy-tUQvm?BwSZYD(Y<8aXS#Jy*YJ4=W* zX!?vjtz^r7_Et<>&LgelA=jQW++nT%e9n}~^;tj~m1Xa_zO7Bx)>8auHl*{nwoTMN z^~bH7`{Sif7PqyN72R6C`5E~&Tg&2~N$mtURc?ZsRVv4hN;unaQl7I~OQ{3yaA%`0 zrRM>6E9byAa^ZmPVzrgO4)EB<|2|eUz?eAR(#Y|G&RnCD zb2T6n%^5=UX_sEHWhdvFIH@}!X4-yIUL3?uHNJy{9-`z@ceGcKzg3L+!{@^98;l5Q zx8aa}hurC-Tzt7nEA*BbS%ZVKx{JYP7jQ%}6H@0`wa`rq19Yd>grw^v2Q`XdIJ8*y zwHz~Y=%M@*<2iD7n)3gmOcM$wR*|-t|C4Dsp>t@*z(TRj;O%)Rg>iLa++NJiOayc#hc0 zy2PZUFjn}?>M5@f9@*(r`@Eb+ryqt?nVWuuJ_2LC%;&R_doy#dUykaF^L4weuOsVH zo~CtED*j3b+`6EWZq;QcXG%-VZDV!!nC=0V0cV(e#OoPh(R&uMDa%>lCdGzMeKA{*hb z9sr2&u1flyaTf@B8Dm$9?0J_I+I_Fxb*tRy*cf7B<=`1)q=}t!mU?$c0?*R4bH+*) zE`y?D?X{45ZLhD^U+pp;1+||n$dQ(@GUzP#^^2A7cqEh9cv>gIjO5N7gJjb=_eSUN zLDKm=ay~rF$sk#Hp6>49AX~#E-ya_o*=bxsG^FC_CcSAA{~_a_LDKz#yHe1s!FDe( zGr@p~LG`~2M}}FFE*UK6E}+M^aQKtq)dzRoTVZMa-{Nb+-ocXMBF{3|QxTJZ>E>`c zXQH9f?-KX_@0nc0VAZ6WLuD--Jc!GCJ8FZZ2eoHQ&f>%56+zAl!zAD`Rl312J5x<> z-1stE2%kyI%h(Dla^SsIh_`}d{bhH`MBacY@8Ad&tHAW|7us@H02;Oudr)#3U;;cogV)2tK#*C`&rp!C4whSuJFiJ_$Ud! zN?PnwBhAYn0n+{|y?B3)a8|4SlW`rh{{3i_zJ^Z^psjcWAt?x%u_aGj@vqhuvBX*{ zZ{8l+g1n$t$S|jRNdTjhgw!>j!33rwrBguYu z2Sn8#WA}G!SACW3{P$0XqL-QKv>`;-l@G(Oecz*CY3=V!NbDGCz`dTmq$wjDc|V=L zpuFE|rfJ?9VRCQE7+L;1MbK%iT;K+0@Hh#*!SpM|bxN}RILUjRz`w^ylj}(Pm5pER zU!Nu1-?PcLD|H83>lf|K+N@0LA_wfPPOHy*XA|F>9H&vu#MH`pa)L~~!Q9y#0(y0V zT~7C!A1O7i$6O57tSYRb!`kMl%r=>QWd6J3R(NhKgx`o zG77f_j4wWCNb;E`m+rE+@nNH_ZPk@>_b5#!z|HP46F4}twRcL=zRFT|+nQg-q;TZM^rgSzGVngjoBw@4 zO|t?MvCo>BvhhA5f1D|o?z;mL?VD+rkBqy>oW}0Fbk;D_nR&Jxf8frYIApfnr3{-b z?uQH#rc*jHM>;;F=Gl|>+;ZupE0NAL(#!pa?(fXn&v|pMY=6W})>80@nU#1aDU#v| zGZtgabY@8`eV!n2(E?fhgj>ub)5ONyewlFFw3yIeL*@!;(I@SEcx07a6OpQJyI5T}=7< zDP=T$k<3*&tKn9YQtrzc%V!9kwM4mfQzFb~HzE22M7bKp68kNU`_jmvN?Rw&rg15g zz11Y1XYRr*+T+hOjo>km55LLc+_6|b`iq|R?KfH~d*2mvCR`G?^e?y5;rwZdtb9&) zaBPWPy5HZ={?o)qiQ2@gV%N{8Gcf49xKGy&`%9f(v=;__LxCvQipWDFd_Xa-|aXh;q!A=h}Fj@Tq^yz zHz@T|d&bkRq3@dB-TKDGE0%SstcAl94u_5g;ptX>+$XHxFgUcqBi(*5(phS$yizsZ zbIPsRXZ;)zDG?zpfBpE-nAQ2?6>PawD!rhA^;s&dUbw>tepsF9%j^u=zoz)67BxQ! z)=g-AN0rat5TeO{o;1t1X-dvzAiD%bRsL|ityb7wRFfhv+%sl%r#GZ_*B4kj)b z@dOP?kf7?+4yIKV`48ixWHZAGE*!?!H|Q|F)&`q| zTykqxwKvT5!El*YGqzuAqOXQw$!?+qJMKHMF!NaRgWqElk;w(o3O}s7Pw{J}vb!)= z1u;dC?yp)3wBLgUVkpDs3a0}Y)ydi{{(MXjjz^C+R)f(3k~*N+f3I{g?0mP z*xK^-wV7>aZ*IOTX`lnrZvQe@tuWGdDOLMuQ!omrHl5+t>wEuz-;*x;;fZ+9;f zV}#E5*juPJ0(s-&U|Q=zK_=mu_wFh=zc{?wS+Ku_ilbGt0;U`b$MfTs8U>F}TjFK`wozXIhWQDZ=E8Ds5& z1I0%s#Qr_HG8hFiu{J_qqwpr4k(RE%WIFWBX8wk!c;wpYGV6uFj0*N+&}~1kQ=p$} z&Zq_zTYeTkv13`gZ`)O8eUq08l%jW@f63APD2IfUiCzMt&W!sqzY&N|0p698~* z_xQ%RV3l{N!A@7uh~lu*Ek9MF^RfGWfV<-l21n-#nGSt2Mubt=3Th1GOF=c z!`+Y)l(ij{?~iWCl1y80jHuy{9i?4o8bebF9rpm1{bGjer>ZRbQP+80?JRvi6PI$o z6MuSU?t9a%U>*<7@H;siR#T}yNtZAMfaLhO33ozjPttYF1Z~YJF$G^~{Q4^@add-@ zZA!udRGgwBBf7fl6lISGp+Jri5X!HBitRkTRUYE_XTWp5ppv?=lB&n6smeW_)IDBJ z(ZfG78r3w-GYSk-0llsEPAU#k&9v7DX2={2`OXQA@-L&h$dyxaCh2b4EgPKiOQ0Ww z>FiRHPE%+A`lsi2i)pEgMcM_oIg_V1D#W4Nq`Yo2P6B^;lFmrSWEs7FF$pOnP+gRhr^~nH zuh{c4TXr#Z1Fg`nU{3~bX^y_Cjno%`d zzhXQ>J5okSkAPGMsvpGv>2rI9GH)CdG%#k6)maGsg5T zZ4HKCC7{N!U^Pvbott#!Ju6*8lHar~M190H+O*6&Bjd&WTmO(#Te;84H@7NB`O^&V zZgL5#W1c-U&-i(3*A^b52*;(nCFvV=ZP6!yRt(BOPzJppn^fQQQWs;DK!+(iL(wJ- zdvpYhpFD8!n7R((a{ozKlhKw*8W?@ta#-7cP1)v=N~zooq025(y%}s06VRn1;p9gw zUBVYs6b6fBI4eTD-@jki@M2cNEcBe+T7akPX`y{#>MFf1?;Mpg>;H~Fw99Cl@w;lm zV+U=Ec-t~iaO1P&o;%xy&eApu#$Y}mQi*B;x6eskerzrada)q{9Tz{#!ICi;pp)h? z9g!bSyZRdCqjQv|soO1Sz+hl|__-hS9{S|8xsfXv=Z@g8WyfI=Z%@lQTsWoZ2HU8Y zLy+;^PKtl(SqcnsI1vLX?ORiFX3 z{q*IsIWv1JOIWU%?3-WHs^Rxg5=Q5W4G>V|L8iw7n?2LRsR&|5UdnTDz&y&HOY|AqR<^d0U!0m^@z@fNkw4@R`YQilA2E8q zCvLA#`ucz8PROBV0TDrujj+J$@acp^%o+Bh|hj8oi2KU#+0vS@-I+BXH# z7k4!^?@&mc12l+&mc9Y8a?Oj+H0@pgH+JcttpstvR7Dd3q=V~`3{OWssCX*WiECd- z024*|{VH2C`&>^X`&2atM#=hY`HFSVT7CL*vOE6$@RQv~e=X0F}3Dn!(=KcGx$;R4ucveJLlez{5p{#E2hxv Q_ZniHG$-PA8)7Q{13M}pZ~y=R delta 92667 zcmeF4cX$<5yY^>xvLQni6af_x5dkHl6n8>MwiFda6s#0cLI@BbBqRY66q8s%M8!b| zR8*{hf>T<32OziQgGM<3hV>3LZ0tp#mUZ+`Q_pBs&-*Zup0uekoF$xrT0ar7yj zdtvKHi*a*HPszKqzT-4-oQn5A4?F_w1m2F=225?}I7k&oTWkT=gMV-F1zKQBKo zCw0uEoN?I`Cs5mvW{yL%E5>CPr;eGJJ$4{mr5<&tslCAe!EK+uqPgQ7f_e|A)VG3a z(`-qV%ZH&P^*e5m0B2V2ss(nSQUN_SwH3?Ks$?;%g8KS8M|n`v$1gi$E3E z^%zs_^HyJJF|(cHV1 zXzKqj1I2);Jwr(^GyM!9Skdx$$2kss6HXNs<2yOdvEV_ej?*4Y2IH#c=mUQuz$5tg6Q*~Ax@WS2}S&Moc&SHnl=7KZshra+;Kbn8u$}Vu*_@wn3bYyU&p~ME82if!DgUJ zD?HV47^jN&K{YnFI465z_UPOkxQuf)sCm{0RQd+w!)|_1r%CxZ%EpB&F6w6n@)S@8 zY=SPwJQ0+m9tFz34?n|kT7tKMbh+Y6P=-#nc>4h3;J@}Zb$<=2&dM`Q`tgiy4*gn8 zUn$>jU$;s5xst7dH)f%i zdDAv@1OBHWe`GxNmwzoOa-4zS1duu^QY~IF$@K91$&N!=6)Qm|QAIhJ1`Yr$>u)W27G(h}q+mMGW4z+j^ zC{ueDKgl|$dFzLbQe@rm(+bmXrN2AXN+km}intFo0YfNU(}#=_75p0@spqm&7VMt_;h~WB+g*PCHdG)qkN{t zhM)$iH;HBJj-AX>wd5|N7v<#_Wsf1hEhpV?)3AB}QHxCJ?d~=^$3M5r>um1n;6ao+ z0aRDAL3R1%dksG2S2k>3-ikUKpw6WY8jlXPElut>ZAxt7y&f=pHmGq;97YnG^A9$C zVzWWRbB#N4U5lN`sXLplw>%EjS~=uweE1Jtb1OH4B_0A=al?lTF`UuxoS2jyV7 zMMVWgskx}K4PAT_sOoZ;nbKE-^spj7x1^|m=6tf;Y>}^6 zO!$4m?VmtDl=N4vFcY{Wx2RZ0_nwc2<&<+6DJYsmLB}E};U$zVtIs8)?3D2Dl5s@^ zQ^t<#Qd(jTU5OL&osXMQ%qz|=D#|W%ZicJjQzuTQRL3b8byA4Ml&4X=2h=!?e^MQ1 zd%D6#? zNoD!jB{}2XfUDeTJUyuA=U18hCHaLDwXr%wsX#663)TZCPsyE|rjpyuUGJDE!Hp3xKYCgna(#@6JWXK^a1L< z4iZ)_vicy4QBcD@zCd-8=eclHj;m-&K*JQV`0YB=_&pYn0%h)|{tq;_GsU!lHL_?& zA6w&yB}LiAFB%i*Ff=A_Vs2qkLB39?sV|vQCrr$pg6SNO=Ibo;6K#^S40zd0st@5> zeS5DrPO}8mp)k53Y-M@Hc?2{_^KF8k;PQeUR&RH=$xu8oyLcQGbs(Kb{7E)_V*j}p z@nG}{P~KVmnn_n+`Prb%as;R`Z35OOR8c1kI29kJ_)VK0TfX@X(~-pUZ{d_lWjQRN zRp^?er*1a&B&Oj%4L0OW)A4^A?tb#CBnv{oh+Ql+KkbLl7<1=pSFT{Ez6f~wayCH zOz`~dgIkw3-eyuK7C@h-tEu%rFecbbyiEKpC{w<$-R#T{f^zBEAk(L!5R_}5Z!rUu zj~orQ1nYx3#5ljfe}AImH#yO&7`{tRQz6$a?2~W{%-n}vCj<0Nfx;^Db6i%956q-&~aWMoff5~f0zdB z1{=eP=V2|kbdlBPf?6{wKy`PT)hiq~v}a;xTTOZ4j&;8+_y$ltsQ@*v3P2Sw#OmuK zZrH=`=$QtlYt#)J&<2!+uAoQCR}Zdo%HXopUeYW6`(!t?#FiwJ??dz^gi*k8gK_Fla2KZzY(sXm6g_v`jK!sehN5H z1=rXXeFdte&x2~fa*KC@D)>t9aBu>shIV6YRZ;%v?(%5oc>kmp&CA=Ps=5{jnd<*} z{WUs!Y&fDh6S)#3*YqFFz^-lLhV!<6Q`4;si%Zefnma+&Hf4&YKlcG}nJrO!t|_Oy zBC%ViAjp1u4lxFScU7#E;al`yEJREACTne*`i~spp z^d9kQYoCrL-^rj_Gl{2a>HoaaOk9z6I^MXNy&^5&{}{n(5>s#`UNAAl>u9#RQx2*j zpLa49z5|AfP@!3b5?AQ|yfRN*kxv_!mottM_g|H>GlUJz%gIeGDK9EZ+?p3oF)c_r z(e&iwR8!%57Qe?l>RE|x(0EX8GScD&77Gf)#~kOagqe;p+0O-HeVikL5mqdqe&=F4y_=WV0&isZwL}CR}6pJSfv7rJ4Ls z!sRzhL6vtms1_HR<=yF)9;QFKVjcn6WEQA|#l=NoOG>hf!Uiq%+qCN3|G%50qjSxO zz24JQ_dKXlbOS$%+3jZ<&O8opJ{mL<3L9f{O0~ zDqmvNexZ(;NmZaa=nSSq+66h|h1b2`3^JzM3aV#UgBpSjpiK5OC=)KTIPhHSM><^c zxd&YZC%%1|50{@^dY*Bs8!S(hn=`IpLc_e#*+r%2n}T2QH?(f=KJ5R{x_NoCA;#Qw zK~=gAlvB@*8bA33)HtsjYL>fIpc?DZJ2m#EVW#qrLFuCk3MP)?lsvj1KfKs_*Tydz zZp!y9^2D51l9%gWdsNFVci6IuOUfqZPG;4oAa(H05oSaxFE(v@7gSrOUF?5-R8}NE z+dr<&vE@s1?5~e0&m*NDW6DGyiJSotkS}{0OL8O2?U5S2V_$Z!9QX^C_Dm zwYRYvI9*9ZX^pTOaMSi`uCzR)Bn_jkLP0QMatPtjJTtDfN>eN0w)*9teh4{>=m(JC{s(iFED)T;C%s3zQg zg14AHLB)5OX)t@djyjIB46ZpdhFjFsDMfkh;qstnpgi$So3HM5 zrlGfkM~2s21k}@6vrW%RElvRCX_tViFsGnkLLR5wNu*ar`9=C#&~YZrF%?_@YLNTh zVB)jjvgj{x%x#6Xh)AdnP|0|%1vtNuj9pNgb`Qv6{Jifvh;OWOqy9PgD z47B1&H~gxq66{R+kzgC}6i^0i3AP7+CVji`et>|M%Qz^*H3QYoc$MjBs@0DKWD-Gm;}gjY!ecrksyJ8G1OVWp>UpCcZyh?=VDHr&L2$?vyG>_^X=Q(t>G&eu5gf9Zh?x8~QJ};Ppx>`(p$#5)O<(&VbNq4JF zza6dyv?5-XosF)c)pug$>fxG~Oiv#LRlu^BjYTSK!v5=xk0c%)vfwJ<^bKYxvOpEw z-Qwun(Hw9bC#OK&;0}MO*$JP0#Vr3r?lT>o^Qv1Onz+epW~_dD)y&2(EM5kxGvt*bc4#n-Lw=~4_LBaIX&hc z=9dkNN1tJc&h=*u%ka{uI*V8@a4&Gv{Pf{*_X)pjc--4dL{B23VYYsL4Kefl^o!!| z3x3%}aqkb@@#I9jcLpt0F<~X%Ov~yhulEuxEs-+vM`M5Wh^&-2UZxCwuO~bE1w%ad zIzRp5xVHwmOPJOj9*Yjfu4nl(re}C}5$cT|@z;%rd7r@4!DQc^oR)%jsQ4sbS%$(+ zQ5JWBUvo)3@?KNF$H=VcF*y5aVM)^nsidT^ZEwL;UnJ;tsGF9|BGV;I6(gkH*YVwp zVqP&!CX9%=%lw+`xc3>Fs*MEh2^g8#t#WA5sF*hf))!XSUpF-7Erls%aMCTLgtC05rA&sva_PME_ zJ~kdbgY!`rKXYt`cNw8h#3qNWTItsi+maQM>X*7bQxr0J8Ip%#S^mE44DV|~vP2}P zOqa9e#oY`1vb?x={Sn5%uHS2DEb{gd{@%PSTs3`s+&#-L<9D@RH9qb|j@&;E&!(k7 zWSAts7eh21rViDW7kDqg)PQI>d`;McHGFl$As+y<1|1!XmcvZrstKvX$w99S%mq_< z4EgYwo9?IQ$KBa}S$^F6;3zx0G`Q~Zehu1Wc8_?Ny4U!ayTUJ<6pwy}c5-N}4o91A zJASXcm^TU5m9&lhb<}?lXN_}0mm5Xsj6{vz!?4p5B}RU1>-Q+k@_HU)8ig<5SEVp@ zi%w36d5^&~Cqf_fezXjKoD}oUV-vf8IOFq6VOg*gnIpOb7ETzi!?D%?;T*ddrc~kl z_wImc7$`=aMkyRzJNo z9@&-ZPcO}iW}Oh)Eic2noRBJHZ5kc(o`K0y%o}|FXT^n$JhOA6k<2*ndRRCULI-^T zCKI^9x<{E8Wz*wc8g^2l%m|WCg~^N*njQ02z|7RdMv*U1^rv5%<+V5|F$$H#V$m~T zox*yj5|YhbzjAcUeZVifEbhIlSTx43Fy==5n#fRFh>cYKExj8yW?WsbFlRN z>{L8=sL44p=G_T98^#nI8}sTitIju!wd^9l>hid^6s;dxvv9I)f^y!E0evgVQFN0pn8JIX1$J}XtRfTq^nhFNtWWPscmKTpDI;Ul~ z0A^fvU1?ehM0wc+A8^yCv&R)#Zacs1inv#d<>j#K805MiCTp5bY}?N&@wg83+>C+- zZ44}IW8j{}3Sr~SB={Vr0cC{7$GjsLtZA>MZwuzvk+AbTLCQ+MjV_&tyF0V!!(84EGVg>Y8}um%jeqYqGp+ zPIa8~OmQ@LH!SRRIAJ=TW*U{G*%!@*_3>w1k>TFwr_ZE-)BNc(SyWCpz7-8m04XqI z4b8ZRVd@7hpAd_F3>)e1yE-F!Ib%9TZHazPXjs^)EI<9axO=%@c3nLBTz~rLS6`Fi zcJphlV@qRUJI6E<>(m`!R=Uvs?qI)ac09TQjSV_4BU=AVx}dz#(+TwmLw67wqFTIt zgzVU5$D*AFY7*}IjSX*3+P$l)^4I zrQ$ixxn_T0jhhp5<9_A4Ji3Hw zbxmmQp%+lOI-GoALi&Z!^iUe2S4pbx>X}Tyj7iK-pQp{UY+gK?HH`18{mh#)+)jQC zJQyA(XAFqVPfx$M-06gF~juDUUUn${9342$hNCK0+T45>4X$A;;Gp2-9z zhRJsB51l_wS|+9o19I%tJPoWu0im&~HS$NEKYdzO^y2ZMuQF$!CDb=;>#s$Q^Gulh zv0}%$GYkzXQ7H3~p2-ACLTS_#$GIa6{XuA67`hp6y4lb4d$PhY@1x77InK;5=H$}E zAU{LsDu2cuJzJFV?lxXi*9osP_-#I?_?z!;AKgc6Md-aVrsIEMA;0Ypm0g<9{zK@p zFt+%z#Nh4RAG%~l=wx*L9YUtz{c&a!s@@+usa%7=Op4wchGeW}6?V*`*AgbTh}k+_nCgx!||5S&EX89M)7g^siOd&NBkzs+Iu~3 zFl#m|9UE_vU$!jnJ{SIO?$<1fN4MOlGp5Yh`=+qd%(s^bnHIF4t5|FoEh9ujvEo|4 z?2)*4$jxT=bNqEVG4~w5=8<@GF4`$d6Z!3CfBK3ncZ6THBJM5l_ivPJsqex1(p>Yj zcFerQmLls7`y0CnmPJZ)GJXATtidgb-AnoUT84$s=8RPHSlnBUbRwBq?^$ENhOwDy z2ZkMUs~My)E_xZPx4&WT*UJ0D>V1{=Fp)O%Vv~?|knd;S*k6`L11>F+!8sBa`O=lvn4@_}9_HeYg z0mgo8P4F&Ewl!XR$Q_9T3SIKfgmp71*+wg1C+a}#tsvCX=)+@?Z|?A?ug-E$^s83K zqoWtfI+^(y-W`O@gyA$At#hXiDDt22ghHQkU-4_6jz=5brInl$jkliAi2Y4D<8D(q z<|6;)VtP)BK1S#azj|&)D5`$6IkhpD(;y=pa=OQ;WNVhutBi!vLW)zB7gdt zEVr{?wI&`dyhmG~s(gjeDPfg*%<6hCx6q-brxQ{S&1rqJWem%NnA^}#e>NT+e4je1 zTZdVMjCI$|jk#5R&9iau$on}+ke@T@$XIkdj1Az~3~xE1KIpA{9UTAo8$0U(^Tz3^xs*<<5hdvNXnljWfA7`Bl%yz50udL2zRZH>bk7 zgwOBO2yq~yJ+BbrWTQha&mk?AP+O=M6Ozk^k5thru~7e0DZ=*XG3=71iRq)c_c&|_ zaiQ~i^&d)jQYDAEvtY_+9^Y<*#bDIBIL&>?@9}b$cgVvDpJI9P2EjBgcr!=Jo5Vs} ze`WP>C5X0PruOQ%RzfJexrshcD11VR)?F^YpqJ4-ggCko8uW+_F^WAZ~~8b;_$h4kR` zTHN~-o<`$YFX;MVkMBQk@>F&v%&sLBJdVTUqg*WA%7*ZSnb2k`rooJB${?k%6G-U> zz0!E9g)tSW`&~l)6!MOI(zqNh%Q{@*r@s;RR-l>MxE6~3Di)@_=&3{t-5D`&9_$>- z4xPaJ5*CLU7d>I6S&`^D4qF7vNW^i)mA;uPE;J1-OYTzou*yt^@J7TN4%0MZ7+J6a znC!wcRAJ294(l5-IZvA^Guyee;BMq%zvj(&bOG9le&+ZLZv!FY4wb91=xXaKK_%C5 zDX=b-!_x=r#7r2&&lAFPgia2fW)Gomq%fBhot`#h$rI({xG=02am~X1-3?P&DS_LZ zl3_hy=0>Q&Gp1)uMOxU?Pk%S=Rigdn)@xzvA5Y^vME3G(rg>4Qo=cu036|jKwiO=KToMPU3|(H65NejE-`bc(Gr$ zEzVPB)wa0zGclUC>?6x#-tjLahI-wKSadinyey6`PlP=8MbjcLeB8}|$vi3H%~3hb z45=Qk*V;I9GyETz+Jh_4jYVTGG2EdW6cf_$@T59DEg2F%346Z~x&Zkgzj9m5^ItZ{ zs&MJ@-hrJodfF|KJ9%%h${xJ1lqo1 zhQ;{Q`LIEuPu)+bFFCm%WwG7~laHE{W}9~nV;Hyro9vhEihIk^GD*=$r~JsScm2J) zvb;9$nH7fZjmy3BVMr>?SEq;%0v%K`J#&Y5A=w9PjeH%}C5l#I_-1B?Q-!}ps#`NQ08sWM@FI}ov zeHZuEBguk|!h5-v+Y%L3UKZ1Z`}cA8Cco3 z@sq?#!x7z`>!<%5kL>=$pZ+sfu%CvPoSf<_2$?JzrB`5@0qlBQj60tt;#j-AESP2- zO=Q2B36m3>73DctHyBPq%B0WDA&jve8FNqbtA35Um-;oo#-nSA>8+i}Yy5@D!M;}! zbNl#J|B1Ud`ZfQFN5_AeI2G?EWKP8c{i;3j=<8qc>>ZrsCLioR>Nlxr?_IpZ9u@pv z+tX4YGQIigs0P+EY%S}9+ug6JiF*@vCfvR9(wMgp)`t`={9c=5-p8=?{jBBJ=6aSv zWg#91%OI|y@4gj_-V5s&-V~g+D{(9E0-^9$z&qs|$LUX6c2d@*SunX1IXUNSg2~pb ztuM#CM!U_rcTnJtVdBHgB-7(a06T@0=Ie@IV0MgHKDvHu)<)vEEvtlS9L(f=8YYj% zs!ZGt-zA)J-50TF9_*BGdM_cQQD)0xls|&?R$bv)#jpA^?p^Y|FrMOW58LUkGZABjYms=VspP`pPLbR^QU0C>t?zAgDTgJyH^G^K;)~R zgB}q#%NzNNIe?in@uM))Id$hdm^xrO-ulc?3*xSZf#FZYZ7ZJLl_+g8{}8#WO%Cx zooCA8Iq=xM<^aNUAkP*1S+6J3QhrMm&ONy~ zj7+7KV&T?NO-M5<)V&}6#@hX1&Zx!{9*0?@V94KLr-h}f4ZZ&~hBRiK4*M(R6EM>b z4Z!a(Gt0F?p2nTYUnyt9%uva+8({JxTE-y!3A6K!6K96&a`4G~Im5ew&;?;TC_VZ) ztgn19+9Bfdq@c4w5h0u6yIAxC7!Moy+_eTZ4e+$yQP*}vmCl6O@nj6%hUw_Q82lXb zTGmMvq;V^N^$2Eo7@CmHLkYjYE;0?G*@NpQN@olD}el!kB` zjBB559Ki`0V`-T<*mD#9!S!}2j7K4M|K|v4f|+w=a=k>G-S=Z&f0%I&4bwuHooTQy zV8$!ykJqlgX(>~iZ;U3u!bz(o)~kYTAT~Bvkg`aQ#-i!!g=<=w8@*Rhi;AcyV7@j zl?#*euv^oz`(R!6vv*!x&e>%AHw*SlE>r=o8pr}W^cD=av0i|9f|Ax`&u5>jvvwHx@k?Iz$Vk_Yk4JVHbPxO?Yxpa~Pg}YBN&{ zhZ81ZIZQTTD;^f}9)+dByl}PI3Hxik>Ue0vda83AOx82&(i+&H{j5=QQv;S|;qM1i znI>fg%$P?{E}z1rUskl?}4dnj1uNN?y$soauV=H!el_x?Hgb+t~sy02GdxF28yN}uC+u> zJd;r95kX}kg9S6)Qq8Mjnxf`p5<9{TAJ!WCz2@SCu&%^0bbDgnBAD^3b+5*}lUwoE zRm6o~0D8M%G7gT-DJgcODbrYCHcYJwpS8W0VX{d$t=x7&Ra-`OV(WyVM91vIaRtuR!5xHT4f8(C7MRvj zrh9hGZ5EVuRaZ_-G@3JHn*N5$c(W$YtNYoy+_a#q8_9Vh2un1(&Rc&mrg-BnCN3=B z8vrvax9(78!}^6=z*~fPt%b*-hFwi%CjA94+ao+-DNIwute4-yWISA!_4(LthMBo} zv1N`Ky(LghTpr8vV$qLaox*PeI(ARY7S_S&G}sA#W<`c~KOq@8d~%L{rd0lnjXhKN zlPfb^S}F2iF%mTm&jUZf%t=HKXPGgRl1ai+HU(x*6;$I@!_+6U`ZP*Q%m)qnnXvG9 z=G|mhv^WCRX7br(#@!ceU0{k@zoday)7c7scmfV8BCopOLv2X};76enVI`RQb1DX`{O2N-E_csIgy%^S=IJP|+b?0AkoMJzKnQyyVWf^xd zUA_>eM$uRvie?98y{PgTL{-UV!D9S1EESd#RQ6&%HjlgEi$CEB-klhf^(Mz7h(kyd z9$TW{pTf54R}al_j}OxO5Iv@s8-9mmPW|7*E+#RC*pZgp+xFLcjF9zQn!g8jK_XE~ z9}{IxMGN~{9}Id;!3L)q8?ZBD{8cdH@Up@0Fmnvlp3wI+b3ihOySrd!j>^Th!c-kY zkNb5#JyE7yGaqJ}qB5U{of9@ptXaSPM-W&x%p6@5_bBYIw)_HE?wnE$(Jo z`Y`yPGr9gB)guSXmqHc%ZDtJ3>I@(;8mNMKlIkaSSxLCNzK0?T3(Zak7W;%_9 zqn$5;g$rLXAyz0JWFJz9Di|$~QMN4Xtb2G+GlZ6&Hv(Jv)q66$#|gTyYUsEba$#O3wR>LD54JA~xZ25-`mja^ptt!p;ucSou_}!wI8YrwEqp{Qc7o z-9xXNRP^iJ<2QZkj~vp%dmK@#4NGxVnwt~!xR|rwpd51=$JvH)+Jot^%+C2;EOON7 zV6W1gJ=(0=SmJxmhcKCx#bRd`WZYZl%XKhWnSuU2Ed`z`;Ox2{-dufvhg_Ab= z4WV;`8NGNIicd3rH6vXNlVh1jwzpu$vDEg~rHRqh*jxzfOUh_?O1#}NG%`um2<=%HmMr7RO!Rjga!6TOkdqMAaVzl=*46l&9 z%yQGhx_UN^_~pUssU*s)Ft%Ycoj_wMf*#W-;?T-qy6}q1V72h;%3v?(b-BW%rVRY$ z5!fJg#GEkxgqnNV{#Tm8Fz3uUFf+lm^=^QfhX{&@G`=cWT}B1Nt_t>o-a}U<9uVYJ zJ7F3)*F1v8t~SM*`n*e_TK=#jD^e9quFMw5CUe#`##NHcW7KWXu%K}Fe;;N}xbmFX zwZ<;y;4uegp83^)cVNoLFmUk8nrWEXb1#SKQ5vrzz}L)j)y?c5V!~I@$=+fzd1--l5kSTd`hmqfanxU#8H;vrQEy?lzb`rZ5{nhuID;;61$S z&3zh{VM)CNrZ}@r-32onwc_4_=^C64;Ym9rxx65+ zKZCk>e`pb*e&*b^lb{-JW^%{5=Fy59kOi^G#dCw{SK&14Q8i{!<0c2)YG$NxtR6#K*LEE zCQYAbMmjv2MyAaRreBM5ubLOE2EA^#B!-iB3ZvJ-1_agD@}<{1w*=E?lAy({)}_o^ zeLB=^>6{k4OJV0yu`!_cI#i?5Ags7qVEWw9uiTuL0#V8^DspcS^tg_~KSS-ShKF;Y z{e07XUbf_F2BL#3bB7)KMwm_s;qyV{<@v$%*>s}gf?&0Z@(}U{kSv|iZXufIbbw$kUdAk`aGo$W@X)fR*tVz3W5BAQX$bNU2 za~fX)b2k*gE(zQ1zLJ)*&}`_`#w;HPGoG!LWijkh5}G+5ZF{Hdq?;SIiwMd==qycm z3}!~q#S33A^X{O>Tsrv^>X0CF zE}gt^k(sxg5Ba*$hn=klZ|_q=n!(2B((W;>4~vN|frWqg7~MvwTQH-Xr_g)Nu$Y5$ ze^_|R3Kx#sVc{tw9P!OCIi4BLR`;3l=k*Pq;vR=xq=T{7_;==d)GB;k z@*bFGz#(CI-@)Wh*oYZ$@&m^8*d$q9#>2EV9ui*Z6g=oU<6t}~Gdtdcsf}R`(T} zOtXc7;~TkWV4eLLd~dglkSR_TAHU4ZcRIhCdo`FGtA5z62Vgp+8PmUO>V^36%T_fm zRI@*acCeFHByzY6at=&;mbr-81JeR+oa4GjO+U@a>m8VOFw>WVA4{BDRc3#fZWF_N z(Gwqco#8>|6Sy)VIaK%tt@kEOuS}TICqI#JAtr9*?I(jCiT}2wy(Bl2Bn79rU*iT%8aV1X&_9ik7>ot zFw>?=zR24O;{{HpY3tQyGPMfUjl#;Xi^yXp?Jk%coHd=@d(P8|DM`yyzJtj<_n*vT z&zSknJs$Js9#}Xx%qiq&sCeVckJaH?PZanF>siKj7lUz&(~(pY0HOpAtL!=6n%C}`R|2Gdf-lM~O@ zDeEXCyw|>fkhTLeA#Z?bUYKM5n=ti1DX8qtrhe>mbSb>~A45pV%qx&fVVa-jhUizA zIS;Ck$FPN%X|JWRFhZ*UvZ#ukY zHr%>_JAfT=+H1jH$uA=7a91~cto531OmqPoM@wKA`kBx5Od+5_GHq(}dhNc9glXWJ z4%un?w;9ZvDIXHj!o_ZRNm|Mq`{w~uW)Ms{n0r-WRA@=vS44jzs;6JQI>YO{$xLIG z!I!xJfXQ*g*Z3o^Z3tEz?UgSsVUgAd|p&Mkba$A>+ zZqP42LN)JA5kCKhD(-E5RNOoK=pz(=mmkG%v3#q=_d$Jx>c)19ABX?8HiSS}z$c(S zwJ{RxTg88LNacSXMjF&G@8n1I@rGUa5h~p`7I$0x7St!iAoUBkb+)qo$dB-6ew6JO ze)Or0(tizAgKB;aKk}AjMgBJ!36@b!xsrP}*}tKxuTQ*^G_csvVk3!sges?r#Y3!K z8x`LiT~RG;Ja}}N@o+2DMoCBT%MJ3sq@9`s$3Po^CxY^VbVY+b_>~N1fepa{5-lot zrsYD7%3#a?4;Xsk-!iI>A1Iq)A5i^^7Gd2N)m*Xlwy*hsPARR062Z2G0*lfe|Q0VutZ#e+bV&;(SvW}sT% z!p0v4$~>(tZv!e_J5cF5fXd&|a{d{%9$^J1Antl7^6sDtNVhx#RDXMk2i0HO z_MZV&`2%gTvn>t=mHm9nF9eltnB^B)eu?E;UKKyu@-dd@fhs58>ID`jN1Wi&wQiGe z$(V{Lqg@W32EJeuy#%Vz4WJ6yX!$0KZ-M;ZdB^fC;=%T{Zu2%u{wY*8`%E(U1;13> z4)I|3F1AY1AD~M9i;dnBMz{LLZQfDQF26J~bwF{CU!-vATi!@K81aqUIdq6tHoCQq zKH6eCP>ng(VtY^@p@PR*Oa)bZXNxC+8n>8@?*Zyl8&z&b64~6K)j9@N`Ljr;1?5zm zub;&M7S95e?`%-LKiA@gpguyy53yW$2z(-__7qtx1y$d4iRq4;f< z{|$r8Z`mXjy^v_tdzVdC8`aNyZ87)R_}Zxa_uKdfK=u1!o31vh{N+{`D&HeX_QXkm zf!(L}DE}&`26nAYFVrUe8mRa;LFIqT#tT*cJC+L-|E}e=QTlsU{}(g^yNv`YaJx+? zRK^c27b^Z^P_uf6)rHc(2G!8-K}G$dJj+?I0aNxUw$e5 zsUj9Nl;^-z?s=f1hS>P>L_nwn!$2L}azOdSc$-kD_=%Pa6<=)m|AcB#3F(AWEKW7~ z${iaBQ76YMY=N~=(ls{YwV--D$EFji;2SL7Xmz0)JlAre(%o#)5A~qYkM0Sa|gN zW2^sHq*ON;v7W_F$JnDtr+V7j^tDkgaGcfu4b`p_YUd<7=bh zzqWe00upwCDsZ=r5DtX*z(J(<1ht9um%~`pHaysJp&ED|s3L}fDtDNTuZ_}&8@)UX zsE3yzNXzAy5{xkkLR3YQEElSx$)FlmZ244B`KE#T2o;>cFInO$P~}_?sv`@)WZm&E zB%p%s0`>VfRFCc>UKKxRaVe;VF9#z*tBpJ!sQxFR`qW10PocZPz>O?H%C*KO6P^J7 z29*9isHk80rELEJRr~Lj?*r8e^+}inDqXVWbwPcEimwN%+(s6g*0l%eL#=R_#Unv| zgetJL>Dj*X<19B>;g7u$+&_}5F zGc5l%RD%W(FFe!YK%4&GP*LacOZ8k3?(z!Q47E`OUTF2&s0Ix~*Xd-8jTdUsD70Lt z{F5yg%0g2t7pk1{QUXd?W+VO$RngTpUZ{d*SuRxm>nyL0s$h(ys<($Cs=wRQmOn*G8q^fG#_| zQP(~r5zy4$XP?>LhAQ1VlEL>teT2%l9h7-K29@p;8!wdpx#je%>yL#6XUJM?JJ`2?N-2}?M^FV!sDmMU?{|>7Q zmG15ob_rFu2tn;z4l2!4{8GiMEUuQwN2mg;t-cmi|6jEDD%c3V6;!$pEPe#)Bb5Fr zsPaEgF(*R;rsrRPYU$UYdZhnyu@e3S>MZ&@r~>}9x{IjAxxOO*f$CTT^yXk&Q026< z>Goqef=toTM$|@SINrv0236pRHeGF$eiFLUceU~VhP2tqu;ui$>B<#Q0hyqB+}B3b zMrAw`T@{>V)1410-vt&g1oaWhr$&M5*ccl>*2W8^PXMuWxl?EpOtuMxGI5F3r&wJm zz0~sBsD@3qx==%MmF2Zj>8?Rn!>R6(zR`qV}>@HMLo z)u4@*3&mfz_=eSmy5e>=6A->-BmOt2^zYd8LRIiCs19tk{C!aAJ^-B{H96A0T>bdc zCfjL~eXVFdLiO)k%Z1`Uf|`{7vAR&j)_|&ZuhoT0|A*!Of_lz02~>dmgk^9m8?R_x zsPfmh*cep#2U&gysMY0YQ0b4c@$D=g3o8F{pguyGBh~WSsQjI+4wi=ryVwlfZG=z> zW0niW(?Ke*#*h)p=$CKM{c2#c41s$e9j zyQ9fgF9zk>(?Lyv3d^qrm3}5D%UloY^Ix$1Um}#@CUo^=KB$J=X7LVC1uq2k5i0#6 z%WI>0ejmE>KL9Gg-1bsYNIkdW_6(|dJ0s-)>vIA{*vYY8&vw&P5Sag zAVgL8hAl`az6n$h-n6<<_8|XMr-rIiNm5@e3>$>bfu2a-k|7V{t5~^b>6S zUr7ITdYwjuS~MM011P5-9Fx4>5D-`n^fL4Ab!F0l@#)#7_7sQfJ~9@fB)`w><+ z(&AATkG6OWs9txl*a_4}sD`InF4XwLY9hegZ1tXBNKzmF`PW zA7Kmd4^Rb1^iX0^V}7XRwNVy099`ucVdBfffC@OuW)P}p9YAGB1(o3hi=9C=>?BY< z>;@{o{>w~iTVGI%(g0BD27>zh8|pi3XOK;Jj!h_3h38s4&&I2eZ2OZ4tDfDmWX-Eg81Ft6P6-6Bl z54@U)5zH%!;n7Vt;d9{C!~?G;)_zq{zH#8yM9mDns(9el#PB(<_A7}pjkwm21Ft6P z@`2BRR}({jIPhxXfmag`yqXv;nFn4?)T@d*O&oYNvGyy8+AYMjeI9r<(Rj##R}*oD z1Ft4x1ihlDHRHgmi5P*;fmag`yqb96)x-m@CLVY-F#=%wz9ZC=~ryw*5W=%oZB4M|LgM;*`2s5W3ES!qaEZ8X_Z7RaRX$Z}O z1=A3ANZ2Q#WzfGAVg59PWu*xGcS$7lD@7PlhR`ZlQiiZcLc{3@t%IS{5f+yrR7+?R zc$Xp!nU0WuDMH(zNU4}4Y213VRgM=0{5K=Ek zI6f%59AUkL?GjRhj^zlYmm|z6N9Y`EmC&IaA+rMEq+nJB!WId;C3FqaD-mW^AS|pz z=pO8pkXDH>@Ct-ju;2=W9TN6QNDum7i7@{Pgk@JEWCS%5`dx`I;wps9V98Yodn7cx z8X+DGy&7ThRS4A*dIjD!2t%$$$iD`mPf#Tx|m>e4%Z=M&PEs<%$kj` zMZ#_g=LYH5Bg~wQu<&|>^Mjod(ym7sI0xavV8I-O9TN6Q7#j4y0b%|egk?7%3=e7~ z^t%CJ#El3ef+aU1?2*v$CWK3Zp*JBcz7e5XLU!QIMHq4uLjGKYoS;fV%3Oq2HzVW* zc{d}hlCV+2*r274Fz#lA89qW@ut7o#A0c%f!i1n~9>RJF+a=@&9dAJ>orf^z7KDOe ztAq}>AY|T(FgciYE5a5DyCoC{=>fvbTM-rp2vdTc64C;Mf%6fj1qh?2*v$c7*a^=m_WL zFem7EH$v%M2y^a6xG~r&p~Kw>nTrtS2D26+Y>}{Af*+*cgD`Uu!oqtHZV7fuNV^AN z;JpYzu;5;V9TN6QSP=BT4`Kej2+Qt6xIL(m(C z_nL4+X>Aml%YaBom0A>~1YR*Mnt5Aqfxtdg)%!h=D}B?#jdBg|NWuq4m_WLusi6u z4x#i}ggNUFz6-WW=&%kU^Erecf?3ZYY>}{A!cRf^^9VDaLs4IR7l!h;({FxjK zdl_Z%ODNSSt{Zsk$u#6;g#7ggNkNr_l=TR$HXuZUybTDeBy5yWH)#0^!nh3xGhRXP zf(;T{yn>MWDnk9B>{W#I61Gcd7<7CMq4ZURIjjg7*=2NZ2PKJ?Os; zVgCC`je_OdlFrZxL_)u9q!{r5DKdj4A0X_J&~Q6KJQ%tiVetnD)e?FI-iHW7wj<jF9>X!hoRc6NL2= zwo4cobo>;d^b>?RpCX(cY?aXAQ-sXV5C#XcK10|dVYh^HgY?f4W`2gS@N?1m7eeYc2or*`ZxGf?*e)SI=(ro9^c#dZyAcY4tr9xyM#%gYVRA6*TZAnVc1tJ@ z(!WEP`7Of2?+~U0J0+xjhcNJaglWNo?~^`?UdsJNgJ990NkOpU#N;RZ!Xe4QYd`9tdw^_5k;h$$! zn^)Pi-5ll^+*iwh=oC+f?y zf;EkkZ>>LzKZ2!Q6~CV2th}**a;rLO+YbGj7dCKEz2ubWzL_LiNdt$w-hJauhUm(5 z^^9tk@?zBsikJAa~ceK7a`vGyL|RTbO+cg`W_gwO*CB%IKD3n?Uk^rq55 z0qI40Z-VqB6zRPzq=Ph(UR98;fOM6vbm>(PDFXk`+Ivm{T)4mcKF@oed^5AxtXb3B zn%T3{`L1(4UqN1oBbRS%%FkN&=-IjlH^!`1QiQ`dgB8JhfkZHFjS7cWz!E-@_q#aj z7gu8~IrWP0iqO%p#`$j=o58oO!?kv+x|1%CL;WRd-KBT?E^WH{T;H`Rdq&@nlK6jj z*m^ZD?%<|@nS3`pU5&V%J*9f6pdMWQT+*(YclP|IaoK$HJ6w)Gd4o8iBxOtv;$~+) ze$H9(9gx$vkALdBb`g5yc~0LUDIeUkZX${Exo?TD?CVJ^hAoz_@T^EOp;z|Jkwl69 zH(*B*5|?t**l^#}j!e6<+aGN_fXVxNae8w6wftO?P(1!^J9YgOXP;@Ceu(s~;B?I0 z-4G9f?67xGtADCa-&{mqqj zp$0_oU7dFD1?%}<4zZ{vfo<0~^v&3~{+OxNtDSdNC6whK`zC#jTR*>`A*aSns|9st z_>`u;o2E2%T}z&F6pijXS#L1@R3O>-RI~M>RwBRnKoFppz?Isr^XO z-$9(Tx#?pULpx+>q~52ip&f=shFF)*be9$?qNj2C)i_a{KD`X>xDi3`;PZ*0>3x|R z6t&x@ysS|X&CQ|3Q^twj!)Kt`M>uV0`Wk6=BjSvq>8q&$;^^#37wa)1^zuxVu9wGT zGPKLkG~N1!s`o8>@1>ghUhY38DDMTEdWX>^L(@w(HNpB`xA%4jy;xJj2nD5VzEnQ9 z4N+fxt|0S?Q>pYu#$Dr-1*c4gcF)kVLeqE8G^+QZX;|677jj=k&}&)0zYTj1p2is3 zBWV2jq)6ReAx?66i09p%uigFGTB4Pva_IA<#@NsE9JWH>*E4P8woiaMl!OQo|k&?VO?c z8(IXk^Agb&5f_Y8aw8%Nr;CQ>y?V3=v`dDT%81ah;y$^hOPhDG6K4==H4}e?WUFTQ8@O_9qwM|RqQ_v7ALky6B?j$Rn z;n!aTBwLH<5a|msEJdQp+!T}GOGom4Xv2rs|_ti z)~JYL#_2ud^f6A0vFon{rlzhA&>-|z%J9|2UQ`@C5Yd*?aM}mx{jeJ1C!m9&l{f75 zq17RkSuypxSWR#PutI99h*e~E4jwDvq#-r}>$L#*>8-N->8GiDHW^wq2pUuq@SV(8 z5xG?)zgjpcS2Iu$T29P5&{S-5kje1XgQnINV5Zzx5z9=wt#6!K;#3BjHg~;bRzoM#}X>L=xlF#IU!;$=A@D8TNM2wAav|cI6t2_TVp>s3M-4Ug1;Y)Dfp& z4Xvf2b%J&dnsy7V46QTv`Eo%;^usBJ$2P{PEB2vAL|a4a25p$3=_shN=nj67h|dtA zw?F8wgK=Wk=~LFwIzrRD_zY+P>#wun>xI3V#H)x2rrmZmPM_m6$=9#bYll-BmtSk-rXps8%`#s_)YrPhZ>e#bi@l+teQjv-ptUr# zWrj8%n!nsv5$Z*ID!pe}L;MP;v(i=?_J!DwnZ~}#&=x`4F6H_lVlPhGv8^*sOR(=Z z?CT9}DYPh!J--dmG^npZj7(G!X<={5qu%1IT;G7ahPK(zmP5;L#BMRP70`;w1r@P_ zG-`jj)i|xfo*7xqF}Fd}5LbgoKz}<7-x}Gfux4r_DI&t&(jq ze={PsV9%72fBcRa+7H+#O16H8(C<-cKX}48ZNpxkj!!$llZLh(dqwQp0iMDn`+RnQ z52U+__z+Tm9{(`xyRe@G`a5fAyRqxVwfZ|}Xg^|~B0E%sUg;Uj<0a#?2YXSIxXXsN z7g{lB@tA)a+CJ?2C7?edvfwm~$E(I^KX$!fbU5ZULpy+7zpA9&%ymOMi2bUxP!U?F z+I`$HPKUAUCAZpd+%>c#*oQ;YKH{FC9mQTn)~JY5NZ0=0FPyX*{R&QLe(`%~*nh*W zU*^z0;%`GchW)5G1|ULfo3{ObjEEE1cN*Fg!>-v|2fO~B8opE5J4kI6q2C*s!=ujS zh>*`2FcjK6%$J7!ckEx7m^tXZG`4?$tGo219 z-vw|$?yHD4JTK)jsd2i5T_;7_nEegyGIsrdgtpjZhW01+@=|UfA~Zy8ohgjdRqR0; z48N4nG}6~VdT82Wbpoo+*TGYnh=|x{She8>7^j=ql}Bl54eb{8E&l4jqQ=l}W7oTa z^%n?Dv+52=j$gDYJ{b((UF_P3wBZCB+CA(#kK3-bI>Zp~L)4}M(I=y!{e@jW$hJdG zLwkT-?{?NEqZ4GDt&h{Gz#p+^GPJ+3t30J;HZ<+AZsS)gbrxv)?FOHJc(`kbISk`t zX!@dyR^6P2_5^!*Li+_Xm!Unyu3wAMdYs$PG)FbkS~2q&+P~Plo2ro4(4IpZ6N_i9 zYWW~)X}thr4XuD-d;sCJnMJxv~+bXfYryzKh6Fd*b5t4 z5}r?*;;bERh`tba8e)W@xuAKkzl=0AH#EHyTFYM-7d-m-@$ZzO6_NX~R4ZL$sFQ~M zJ5KtGF|=gRw3=wuC~9c>teK`&Ll+@ft#AD)?IHH!hNj>Bdx$(ONxi;b`83V?=_Ad% z5;iT?CpARxM`rYXe`QR=KNYACr3@_qy9TZqr85@{pS1itLSAT|lrgk)*!8s*SpV?m+cKp8?ek^#*zdPIW@{K?BebGy;10T@%n0Gy~1Sr=TTh z1wPXI0YAo42k2dZG-etYg4fFb+%vs%x4I zbQIJpUsHhE+U_v*V0=5Q4KN#mMxZfh0-A!BKowH$fGVK|f;bQlQi4<ZT(A-M%##=0tujkUN%ty7(Flh5}tG>C#BAud4x6@l=jyRW_+s zX&{IPgTWBc7w9OX(?K25wt@WctM;Z4Xp4J0&>nOEXNk-?pjY6l=CiflANwVU#Bng@ zFpv{_E)WQUKzg8)C!H**UP|>)I*oY-RR5%N6rGd!(7))!LMIY_KxYj)#HW(Z!(9dH z>WHbMp^kn!+GPdVKz5)5m<~=l80p}nb{0MPRYq9`4Ez z+XZV^Py`eOr9p0x6=Vb1!CA19)U5(DuO{phy`fPvr}=xe|_uwI^wpvC4!R34BQc^zQM+UVmVMAQ~;`E ziUGyIdmsW71UW!XpjW~><>*L;-uley^$VfrfqpvlC$J4{2Rpz{V5zAIKNk&EKPGnv z^DpoOsK)6gv|FGp_NMS3!c?u(NpKeY43>fwU@ce&UQm;L^%_za4|>~YJ|a?KCC1+!*D`9ysQ2-NM2Yv4yBwF7Ji z`dy#Kpb2OS_CT9S1snlJf>B^Ja8Uv+Nziy|`UEfuOa}TIWImuuC{;S~rHQmYm9V@I zJ^+V&QUeNlG@*a>!l-QY*SH%1!zY{0S-=yyx>TPLa+S`6j`{Sak;%%Pw< zs0CCN^bzYt(D3osIl zlKW#^ISNg|Iu%R<0;Yo*U?!LaR6{fu%m)jk+*ntJfNzn!0;~jmKwoJ;)|Dkzl|wba zMGEj7=6T?v;O|g)p~U}J%-_HcM z0&D5c&VqB`Jh%WZf=gf}SPpuUclx=8?*2?PzQFPxgr6xXRWzw$=@6Jq$*OWvzd%tK zd`6S_9H@4wFHnt=YLEtkK|obTL%>j=-~A{+zoY7*%s}tubpb!%4^n~v@TcB9tVi+P zzz-w^{vbI>0aAk0APoor=|CWuNUHVoj(TJ9L{I_fw@i0X74=%|@4)w9BUlMmgXKW4 z#9j*Yn}qtQu-G2_YYRGpW}p?AMcq_Ah^m!pP#m>DRiJvK5}+g~0zDl!oCOW1^d7Oa1iL__S?Y@uoLV8x~P8uZh;O&wj-FY6U|`+sOp+p zK))Zn6NY`@2+&2m?wg+kx=F5^;<^d0x|t-P6)ETZf=@fp9&`Zu^~~qUiNmZ3bg8GB z6DE1{k?hC57wO_JM|qqU0vS>`oYt#K-Cm_$=);|KtD2^0-7ICEkp=%^phJWz)7%G z7aH5K>;S4k*bP)`pdZ@EOGHkB(?Hb#`YD|0U;?NB^p*tu$kSFLv<>V8yTER+2kZs= zz|Y_SI0%k_U%_wSI5^>^{7+&z1x|zC!Fg~2Tm+ZEW$-7s3a*12UC7@$4XQQVYLWR@4q!FMj*a`h7uovha`YzeATDfTAEbs7G#(^d z0`ox!@Q_G$!|VZcUo;F91;0QqjX9P;^cM4K;5;%$LZ1utD+apNs9TG92sAIyJ;F$k zTE8!o7Ni3~K)3DmD9xWBDbI5;^;2WZNc|SfAHXW08*mH3D6%;YQ#aZMgBqYZ*{k}f zjYLTIw`zf!pcihvf$nAX1oa6kR<&&20Ou0OJTMBS=1 zfbMF9>gTj{D?^!d`=Tf44N^i|PHI+wt9bIJNI$u!`xc|YFfbg90QG>%cn|1Cg>FI| z0Y||G@I8p3qHZIS+kx&x>;&J(^6w+?G3bMkZPakxV^{!mUqSVY2Z+QWuot+~A81;2nJK)-Ib3akMS z!A)=o`~tRtufP(}1tcYa$tmmzYw^8WEFPY#kib&}qx$z(++uz76Lowa&iazNMnInx z8r$nU-vYOR>e%JoFsdH?7)S||ni{w^4{sJKEtuzwK(jm*&{V3LcNaBW)!M4YJ_A&q7MnNKq1dNG z_dfrH{Q*#2^xyjJ!h4_^(7+Vx;XDC)W2y@3dPGbG1OOF&ia=G*`~+OYueJ{T&Z71g z8UdA|_y?Nmo>N2DXLb7pKdO742vSYrUppa>_-#L+dgt7+pYwPC54|x}A*)C1^xU3y zN_wVG`-=$3~OPOt;$nq@QQcVI5|Z!z`sj;?7oV)g+Wz)J8r(Auuc`pGKT{)oiHi+mhx8QD-c*K`l@d+=HPyW;Ku=@aTi#6U4s&po^A= z5Yu7a#rzwnqEUNH&3>H=X#LU|QSwdCXS#;uj3`ZUJO^JA5uGKu!3dspBCJi_8?j{A zwT1c1vF)xLDYXMuCyj4_xOTXT#%6$|dFUl;9#;XOpak#$O>QuzE~`So4W7d>H5SUJ zW+u${Kt)84;<-5XC=dxEfbtgxnSt_$VNReW`V_)a5aa{7K^E{U4B0WWfvn~^CuR?6op6@_qSQKkCC<4k5he{v@doiG9W$*#e4y_z014@Ed(@=_MD?Ns;aW3nn z5hZ<2Ne+9{tj`re1z(^17O|53;PXDF3RZ>>fqL*}a1Hrkr>lUYj$GgADxB+6Tv~wU zpc(j?Xg9`e2pWL;;1eme%atW%Bdq<&dDSmAk@mY>9^OY7hD&r;DEaWH13+a=U8;8l zB|&H0Issj_wBoru&}B?}Ikn3b<*z-Q7JqjM+U?2{JB`QjKpVk0Fa~G~90`VkzTl<# zX#>-SqV-ipXoJ%FJCcN}+dtTcK=USC`Bawjs4jFc_UZ8UPvnu!yIuaV10mEgjsuLN zdWgp!2V%h>L)V(@6*>&se+#q_ykEc*hB+K_1klKi#vBDSqLf;!wLmrx39+9F!$dFz zOaK~teO7Zam;_1?Nu?b}kYI-K+=2jBU|$Zt0s1@_^Gh%tXlBmAOakw0%vn0MoQY)y z@G>mJ{xw(#78tq;pO1YW_zElnOTc1Kfe0+cJP6NmuoS3LMin$FOJykkTCfI`hQ1n8 zi!CfC|5ovkjB?U)RjB9n(AI%V*q2D_pIkX&zvoe%w_<8V`vGX>+5)^@w_y(^K3W;Q zeCnn&FW+`}G$Sn@TGyLDQJe97Sn!kQ(EjrN7tI@YCq5evl8ZIykx+B5AzQA7cZ(dxvO;C3nDf@ zc*t`eo^>y71(*fxKD4~hG6Su%4|rDkUzk2%iADw42jNeT8K`TtbXb&8cWG0Dlz?)x zwndU*_XkOV6KLN4gWF5+927v{znIU!Q}77<4K#4&dkmfcb$bB{!mkZaBj7;RLH*>Y z9~P}lzL>g#a08d2sbF0Qs;NAwu9^Q zn9)G@?;^ofXyKTjVrmXVVBbs_nj^b;-UL)_ru;1KEscC1+&S5?-&Hu922l*0gQRs% z>*aEgkQ^y@z?IIW8COyo9B^gfEyTVDTzMjtqKk(2f#!p*2P*(?7M8@W+p!vESx`of zA}CfL-;9#xsM5;=Z6o?z3A=jNB7YEh$pUX% zQ#BXQbQP|}t9k9M0yVH#2i^wxYINtqoaZ$3gaYED1#6 zV@O(QH2CyyiKd2p!G!*5rL2!XZ!9!qZ>XO@uLm+gYl%N?JX$} zRE0oW;-Gn~Eky%t!n3vpO|p9S)_yH}-F<5WGysjb{xsjzpGHU{@+t7O&=a{mG_ser(Eeh-!Uqo z6p5?ycwZ;i-LL969Z_LXVUaNJmb<51Z5@#^_qaQR_kFvcc7-tU9DUjq@4VrFXpRTIsbZmu^6)#tl%B{a#!Or_hB`(q(9PLJq#_eV7M_apoU2~?>@eEF-=&2Jm zs|8dYxAbLKUjm5^3l9q?3#ITMu8`=wFrYMwyF5Nw^Aihi-G(7NtXOzhl(oC9E@|cr zoBVV9FDY?}j3I)N9CW<@sXIfGaG#n|?iNXICTkGITeo6GocV6hZd-0J#FVDH^ayLnQXYy{0+`sJ#bVr62(T&6IHSs>;wkxA-IZqWyP5!IrxYK`R*}XD# zAJQKYX0ZlKxeo61zLaBj8GFGM5?TmWE$H!;n$|t>_5Ja%7PkxfUP{?{!Bx!>BdITv z6GfPlDPz6b`)jmFvwjbZF*aiviNEP8C|xhQvN?*##EY&d=e;!2bdfvQ`6P{;zeoa; zNcYD?I+d&`KGd z@Z-t`otysNwxk2$i?jppAkQzksynJn)yuHgmlDB#Au{N)E2AeX9hKI@PrK(VRW^C* zV#W`$yUM!Du6WPq8JL2=;4^sZ(!KBZ_z8xnun4W=@zUMp7ZN=h22HZFU&@W6vZv`~ z#m-`whKt5~VcmtxzTDrir6rpN!bDPL|4Bqv%S{Y9@~11Kv3su4v^2Pej#+2k|7R{G zYBee1GfJ{taYfm=6C$0ixY{|EO82AgK)HT}Md)+hz)FS3&A(jvbZO7^!|_P7&`e5B z-Bp(bQO9e{UG$`=vU?>lEjH(WcR>9S4v zmddHC#9*CVs7OW1eU0pUf*-9zb(`OM`E){yN0uM*hpCw~x<=V;%xnklTR*bSLHDvB zVbCrDhNLq78kOKd7TJBx73t|i*Q4$BV2bL$HQaDFpVJXZ^rPtkM@YbRLO&t})CiSI z*NH@P>3iKZlS^n`-hlXx6jo!C)V}2k%u1o7-lH*T=ATIZ#gJte76h2o;Br_7!W?=A z2CXft+JAHGK)S8z4FfT|kzGoDNWFZPT@Ku!UPg%{jbEVK>ex(xR@tY|40B{IyZCRl zYFPJ=lG`gHrkGvnN=fybZ*LN&leC&dTThofCJ%qTm9t;hG!2jtLpvmQ8O|o=U8lD1f-J`_?V}cHhP&iM?hAsc_fT)_J?2EWhgt_B@7Bt3%~I zpQOs+%8=7ahBY4g%DKD5Uyob;=YT{(n2yttMp{&*ce2?-PZYAVeErgk{ZbcQ`OxVY zM07R6=L<>mN3P&aQ|`HZ9dF~)qM=b4J}UIV`-GndwTN~j#VWO4@M)3^jkNw#669Z` zIR3I`0PX*x;+K`#f4QnV%^*$Ph=1Rm&M$`XDAKMRQO;+Pa<2%(ms>h(T}dk|iW2dB zGM^{s?I_vufDFk;(S#D-^u_sa|8u3yO{XJ2Vd+ogg7Oy-|A(|s6HQN^7`uE6bKg|c z{VnVSQ@};&gu6eabIm8);P*rrz80NZ}5AFM_t}bEE{zD6%5D#rR>L9-kFV-6(38J@ zpZvSi&RU=7l2Z2nL#Y)kY0sH*^to6rV4G+cZ@gBMmoP+kh9MggQstgJb>1AVywDkt zRsQ2}(EoNS z@~SEE$sjZF>M2pmZWhJ-#(g(AI(vMg?B;S zk2<#=4}Tt0H<2SxS(fYgt#q+VOX<{63aT8G0%w3oP6p@TXt zUHeIeM4`>4TxyzTKlve>J3>xAbA6iZkMefEb)&qLP3jJGGu`reR9?0_+<{rto}_|3 z0Do|?Q>FYnHfh&iG>oZsxR2EwLTG_6eUX_}j=ps1v^!kR{Y(Cplg_*7{y)aMPLh4v zY~6i(LigYC9%&Cw?WE9i_=Zb-T3TB3=d`pG(jSs1sG{B1cAb7W^`H^e1V2T>BAK$G z#aN|&7*N|HyFw+KFKSZDemiE*W4;yxMLXe|@>~VAG>@BtfBgN(kr$V(z?nQ0V|Eeo z!WC8XGz>cE^c?x&i}j0g4uC;N9U8)$aY*C146jaA`nKrQseRASc-wmQ@r(nq{e_<> zyG5FuC@U=@$dJY|+TjkF_R^KnY3FUQ$ttPw(pALJG{2*~`R!7>$yF;mLmXCSX7G*l zIgwoA8Zf6gU1@4ox@KGW)Q{X=;r}Vfcj|m%;hDOboElPvrcKTUYh}Buakl)(hRXt`MZ{?*#DGKo9VVS=(6RFI_cb^AIW){*qSI5n5~ zDcw55wAz{-Q!Wi#OZ7heSM7D%fIB(XI8ZHVoKsvgimXdgvJIb4T#>@Zl_?id} zyL&3P)p>mH_Qohsnk9EDCsVzz)VS(x=UHz=2 zkRs#XRC1TxS21rdhu3-rZ%uaa2VP&P=u$&hfN@mEolu~bHlsS1T&h~fJ0o%|lndxM zOw$dNGy&x9s}rPEQaFG%=G)O89lEb>c5J&NE3-~I!>D5P9Coo-Lr4LbbPO2LYfCQY zhkl)%jkfYVOv~c1bIUPZR?%&Hz9S5sR_(ZydEUyZ{f{MvVbzt8 z=rb_rR7l!zL zem#+)oz#W_eM?8>%fJ*#`A($t3%c^#R6pe!o-turwz(A9Dq-k7&e z$Sj6qL`P?0pWL1}BFV=~zqEtVq5WAWDH%wnW$SEnEUC9|_01*y?+d5b3I`aybPGY%O_1K!GJZwtDV&AN#Bw`E_E<8{ooGIi7by3F+`gXYu<49&bE9&Tc1-x{Sz_3E zWJgQZ1XjXoceS}<+U1z>!ePg)#grBO^5 z^_EgSh`B*RL0K_YIhKHgCoA{M%k11t`x@tPr}b|)!X87ctP65QS|jAd9L${VjF7V3 z+`)1-hr3`7FI$NhaD@b)Il#|GPA|K?WhE0@mpC?3B_U2>= zeLY8Co4a|JFRzwZLQOMCNO<-Y53y%`8=c!-AR%IJt~7|4wc3d^L#UN9E8&$A(o9C` z`28}s+ojVhcOG{||7_#zU4{+vaUOSkbiMKRa;3?M(_Lp(bo8~-YblSr;G&D3$@%2B z@BSz?#p!4UB{h_F(jt{#VHf(>m_AY}FL50rEf&)*ADKmB>I(0lNnPr8F{@pYc@Z(= zWHfx9*+>XLLdsm*N>)5txV06UX0Fdl*`^HJVPMZSE~ssqv$gV9+2oZF?qjX`g9tR7 zOA=Gk$-jMVPH4rrN`qB!2zPih`3PYb`A%VkPh_{6hnbCQzwu_>x2mTdD0;`r1*khiWMUOJTHOdA z|4Ftsu5jw4pJti3?gnC6duWBXb|b9l2EJ5eOL)ez8=9S*tjYtU2E0B= ziWFp)`=OM}>KDQly72``t4C%Qbhl*#BuOE{DQh(wRiEa`-9|(L4dJEf zcCj|tcE>gA?Crb=*VQd*-Qv5BA@}4Pi<_u9Z`v#U$n6S+wjg|sMU&#kIi!9ZfOLX<~dyA!17DFKZ5-KBf=Xw1$c`FvU&YzAAz_8B^{auDA(uik6}0nx ze4SZ8w%sIuYnx`{oLXU&d#^0%LOk!TL#<^8;!NJ#t8=3P50KQQ{%djc6i->a4wfeH z+4@0Unul{m=|rW3OPSnkK6!6m&OQ1{A^#Z%&nL!oft*u5yF5MXS6Zh+;_?sblOnEA z_|``2u! znI6^aZHTPV9`(@s?u^m3zqO}hYZq^>+qIV85#kZfpu|Y=*3EI;bPGDu2PC{mx8>{q{SBiU@-*xS{xlb+r9Mv3 zc!zX>(c|1{&s@qc95gjwiH%`!=%7d8rIqp$cJ(*q zn9vF$WR-_GYI%wO7@q&iI$fIi(dmD^*KY1#9ow7t!P;L`lNUiR!<^G4D>R@wvZprL zX17@r9`V;^YT$rOsoRP zwlu%y&C)sNCus@UOq1-BR}xx>!MuVLtwZKlla6ZCmAQ3@lRaINgLP2Ww~*VAJlzl5 z(ngia8TH4teAnq5Bdlpl^kB(W7eA9Fb$8lTow^8JBwZnedcQ-kJY~f{Z^+_k>pX>Z zpyKFZSyXSX; z@*JM%cds;${${t4ZQb*nxfNSQr>2yJ_Lq&NY6FJsH}=r111jmYQ>!lr8xT+~xe6P{ znLG`Nb%Wz}qx>g6*^x47%1uCAgdQ6U_h~6L8^YH>HXk7?W;JA5ocpAm2WM_)>U?E+ zYhC|puS+KOkfTUrJNc&iX(?G6k%)Jnu6$dl95bO#^_R|gMFTp#k$07+O*wJ>6~dQ# z%ItRsWl{Bee+*(bUzYvZ>Gc z)oY%1Rs5M1@r5(?uBtV)OrM4E=MEI*8T4(ho_mWOi_{7k7QvA|Z#{W^Z@W?Ix%J5SF(r;jnubUKZMvjk$Aryv{gU4RN^Hq5l{D@5Rh@I!v#`bw`C-(*hDEs?_Pbe14`i!=9P2%GuWL z%zDJ>1w?!BNat51OzWq8DvgCx)izX{v`o!($uKZ&$;geHI{j*zEm`~j^|aG#N+^1C z4+j)N-+DB}(na08A+^}MMs?A?E%n6senlUGh?{NQ@pjop$-H*7QqyD^D2}yrN5zc3 zZP%G)M;aVU_H){?iG9uqT+)%OwMh;v&6J}4q(qk+x21G@cVy_zJN76RU&8O?_c3#8 zz@Zu)I{HUR&Hx3@9rT-Nw%(uUEgTg%;Qhessy5YOn>>t@t}uGOG7MXb zbnWd^sls$SD20y|) zY{rOUIj^s<#IkG%XVqv8Jl28G5YHm`vcOkj&&aY*Jh?69f>n*}dV(&@ewjh0^l%UU z|4reTAxX54rE;w9$RnQ5nC}*m_Mg#eIHak@7CvG?bvph~?)0a#h?K&8N&APgV<7X6 z<-_PsYsFkA$co_G)BiL+;Sc0PH{a=3JPVd5_3Bem;=$OZL zcd&Mqd-87)?~k?wS+&gO4AR-wt1_dAdauu!o0&K}Y)5I@hf@??KXYUbKzZ4i1ucRmBV3H z%AHT2$t>b+${QVnBc9Og68Cid*KU*j|M{!Xj%JJ7kQlSHiIYtIsrlnz(4j4CdbWHw z%4OFTqn0U+c9GQXPv!bvhGE3)hEJPN(6~O?UYt7U?ZGMBQ@EtT<=4gY>(q*peI{RV zu`6UX4uq5#Ku{%~+r93KCgVT3`OEt!Ok5aS?E)|r(DV5VTU~Xd#Y$JH&lVp`j8dHZ zpn@jCpdluy`}wFV(?a!a02Al=T!2pEmq~K1=SHku>pi#erJY~@uWD9lvGPDW6HhDn z7Gq#Hm1Xfj)-0CSC_5L!b-rn2nTGV=p5kP@R*Mg%MJ$Hp!FtE#RX-NtRVU@ODT_RSCh!0IGVx}JZhHRZgu(kqY?e}bb@L& zXq={&*ZYvvMsaJjHeRWU4iDKX&QE!$dP#RIV$~i7{RRZu3qM^O9LoY6zX-S^@~b zW1m6=ZVt$w@tWn4JmA)ocUGreG8(U;C-EAL*KTD8e6u~aL@E;|?f;&yY#%~4n~ubj zjO);~4F;BrF5heCVclUd$+Wu|Pc|6R!r)5#*|+m%zV9ve7*6&|IVxL1*KaDFse-2B zzq{$$Rni_JOisPKm$bVv6IA*nMNE^heZSV@dEVW#nMPpOH*W`N6HWi?G?maaN6%Xa z9jgvll`5g=-Y(=dYUsQA)o>qgpeBBHy*A^e#EkIv2&Rn&dxx90GIs>c@~v@vbzn5D zII$UgDA}lQCduA$*;GOk2Qzw@BDG>dp|pOjb-HRPRegMw>&A zSV#s=<{(0SgOtNgtf&1`ExQGL<6~pONXq6d3MCa zPpIlKh6aAtjnUIwOrHmkXj5H8LrjW5}6qFrGjFvMPp?_zyKYjXOaFs7c4_h;o zq?mUi%zDb>f7;%8D4aBDz53}(@aD)M!x1hOrlS`4Nm9Deg#VI5&QEtodg?O^&{|h% zSId?M^E^P60l1ciUZ3}Td4s$zHw=CU(4bE=?6H45a~fqxyS z=yOU)tTOzR!><1CwOQ74a-OCgU?^h0U{2rhnAOOE8MLkwx$V}qJ!3BS=4Q>=%C)o# zG{t%1nMume!=O{}VQCt7>NM10odyV_yd!*W=9XGBnVCF;LuVYzQXGHrvcj<5G|d&T&9{Iq|&o%N?Iaykw}(SbZ(vaqY@NzO&@ zn%%7$?Akde^*oo;@dyT;iFJ|9gz6~-gSM22WcP38e?QRI=?Ha>w#ZI+MwS35-PuE+$m^JG;R#3L%&3Uxh!Hk$YeE=%7wY^W}JK|H_x5T(?8NKh<@7w z&j$tsJwcjTuzV^5=fR#w_Rk|{K8UibV$IL<&y7C1>V=6HeaS3&2?K35WIp{t^^tb$ zhOBOVIDhe)-x)q;c*!Lm21j~1JfGt^uXGEZd0dR5DcVx4=>VU#^>js}S}zi?Oy(}2 zpV|O}7VMXKGyl3d%X)j!Wa$BR;iBD7lcrUY7uqvW&)TYH39rAz$ejgb`NbGX`W1Ve zNu=vnMAKhJenq##6)5^NQj#ykm?F6rvSGVS&Mb7toA=iv?LIcd(MY~o1kW=0aS`zz zVx7Em?2`T*z;hgusf)?rA*wdhQ4kf!WQkaUOQ2L;;=cYry!rUQy(b!yxPg0gDY@t; z_g522Mmf3xgKvI*k5OGZe~r;h_UltmnXgY{Wb@Z_b!#PJy}N*8r)2ro9g=KkS%==& z6?c~Eyo~B>cE7qUql>leyqCRZ1s%xNW$uFby1&*PU&H>bS$O!dGshua43S(X?hJIfrgvL8N1uRmA3 znM*mmS$LDcwCCDAGyKQLgOlc>-f~VrPZc>OekIe*43+G5S@Z|LO`Ah&?y=gDDn)$q z$l;ZgWTdQFG;%Tzn7z6D9vHJrPDcLCYi_J5bO$+ovu)P($ccc#A*7jKn+r3$`Y?@e8N zvq~0~7VF5nH)^@5hnC|T?I)qKo0{=od1i=*HtJbg#U6@MBx_wdv|W~e606M*QZu0u z+nE~rE6iFKxBRo{N#D~A(2r_e)Yfscip<$Si!~veNSZACuko-`nGkh5T5@NjyI=w> ztK`^3=h8{KZ=#|Ml+mvkc7Sqn6J5k>ok@c4w?#?&k2m<=%;(p;G9$z!sEJJe?#(uo z5T4ER)*kBBdirLE_+P4<%W9t12@9F9!<|&BoN=d>pEkRq1r&wG_AZMBBxnXTKNfnQmA;U&o$Xd3A#itU6}XOD&abKakf8q=Xvg z0MQER%2Ui{eCe>);F}ybQV#3A1z)@cq`vmyk_MO1sIJ_o$mJui6rX8B%-KKkR08~0f`+jq$YkRPwwmc zapPov8HT7Ba}|>JRp+;ExJJaV+_v6$Qk@lVAO*_dU2d1>tuGrfpC}Y=y&JAORlIZ) zD4BM6y%pa9W7N_tpP5M~;#Y zOf<>v`4aGx>XIxEL3%NcP2nnqR~&+6(od{kK9<@UILqTFJN{%fb$$=;{q>eV_n;}B z)5PvruIy+R*yB;TyaZrs$y@GmQWt_Y+a}&xB5`}&HJqO}l_PuESBh_HXKcps$~`vj zsHCd_GaWnLRDAcjuEZy&pi+S65 zZAy@%?vGm9>z4LCVwQNu{@j`%aM0sA&Q{X&0QEOzE9rB<9T)lzwF?8~i&nDoS8BMm zCC9rEZ-`9eC|yeHS+u=p7vikcS_T{>^21xpi-Ydqm6K3c_IJ9k4nID-x+uE|9+40w7ysKJE+@XY6IUl!{^@rS% zF}`hVjiOJZ32WwUJ>L}%^yhj6F+DEn5H-Hr@B=gJrmJsU=sWVYkXLkag>`CU$~g%O@`g#Q6N(PP!aHTVfxDF*(H1b}q`Q z?p)YWN*`q%v8to3+x|Sh($svDr)V0?)Oo4iKNJ<(rjsqd&egefTdZAr3kJ378oX#-2#vuujdGVVa(TvC3)totcNV{Y1_2!U}L>_m4 zr0uGUUQ`yjiQD0lILt8|9Zxt(vGoK8G{Sq?4QSQZttX!kdN_yND#ieSW}qZ4+TSGo zWz8(V(qVV(eAH^aS(2ZmyRwe^1p4vjL{XnAy`=0(#5KWF5T5Sc>Ng@!e05!cQE+;z zMGstbqPnhP<~&P(iPhz}nTnK^v551GgdrOY{&{LIF4Xfz3K)1#O2hk3cEFI($2>e~ zpJ+;R%AMbTcW;MvMTpS~G507wmzJ0pYCwMpbxj z9Ia*1S%zl&1d{x9)?Fm@msq>TSmR-;C<1{gX+`z0J_r+CSDk)H3zc$sw( zvEKGzoR7xKjf-eedT<4%b{Vsh44NF;Xk(;RU94&H09>>Y4C?(&RORYV7FjM@UsA>;m}aYVqOYdUMN zH;epqa_3KXuz%L!c5}3yB<~e>T4&bbQu2yBt21bXG`+$hqC8w#uY=RCxhl6kFPFL# z0Ge*?hayMF@+&01Nxb|7r!6&7;5nu#9?}r(%q;~rmF~Ody~K3==kXG9hWToQPnS_r?HVm< zFn+aW6*=<3vdmGni(0qDSw~rBj*_@*RBZcLS&5=LWH#f{W>xz8Z<`yo?&z?q_MKXk zgil9YwD@k8-aa?apzrJx48C4sEk5=6C#Lf8Emetz>bhBNr)JAw`A; zkF}dYShtPcZ`W<3TLotNQ(&yjQ5mJ-(AC+!$jjgMDq7(ceO0}&GUle%E=yLh!(>&* zv6A%$B~x*ntvYD1^yZ$5^AG44r=lsDzsE@zIGp9i%K}W#;qi77(>xfGCPVJ7Y>0R@ zzt%AU>!_5qVU<2J8FKuged?h8^2UE<>-5oY7PqA{eQj4f3;FQw)Oq7>MbRC?WF9brqC-)}LvuA5|!{qc^NF}ccDnQr31m}PD9>i(6U zkMVB7Sx@P82juKinR45jJO8oWG3{ z>ct(#XtR6cmF`T@U)tSe^tCywy{k*2Wb8KKZ6Z?Zp8JUB-7~KWQsO>|JSI}_KE?QM zt*Nb6l`Z!f6q!Dsyzj0Sy5dW_ZvHUl;roGi`~H-8ciZ3@QuZ%*e%CaK;0ZVQ}RDR z*5sKKn3TEV4sy<#Dcv5pv(gz(eBj>Td^AfweCW=fEXi!UvuQkA20SDPdpL0}oFkVX zQdjL+e|}lL&z)Ub{q5d}qM4Ohi1UZJ67vXt`%z{+!pXkq*7W3UoJR5Jw6A21boN^y zIUnQ2-nL-5I{GnnVe|so{+KAwTOg+&yRR{X>x;a;yvS?sA3s$|lF_$W2lt{vyTYK`0?WpB-8|#gL~cja zeI7E=XUIYcf5wdIOE~m&)B7KlO6I?GlaBo=f)bfR_UoCoJymuWTk~TFr@9okeE%c^ zpV4YwzpnJfGZZBM?cEH|v_*DbP&IH@vQydfc2a?aL|CuA;30?prJ|cxnVi2b+LZY@ z6R6PHi|uwfCV2YJx~DQ9#J5iKN$cXpQt}0j+0@xpi=`WnPOg4EhjCA^^ ziK~5lUp7ndXiYJ9FP5!v#vFk|`{4oAZ*`gG_vCjt^z9c!|Bj3H!zT*I-5UP!pOFC2YvYk`>>QsH)3WB8-EDPYrFS$Fg!P6`h|{xN1~~kJo%xo_G>2a#>U&Lz zJiOpa>u(hf*$^Yo9ez>HJ}V{C>8HbAtkW;*zhs+TYSq|*^d!OGy46xE2|?^!Egh2h zMLG|ymN`6|eJeSV#4oM$%xbxu#4oEe-x`U_hnxLkO<2?#I~(#fS-7`E*#(T0&cXzV60R<4!OSui)Rl?goCFKg@Wuf9g@+`CrJLJd8R9IcrLCi?oH zC~lqeFmnD}J1srl?W_%yvVMMCF1A*-Ba>T-^!=$?u66bT=FIS49s71IDw`OI47lhF zXZQ$r`)`L{9G~d&wp_JW<|d-Ag(y$`{AzfD*4tGqN9QNaz9?6vX`(dyO)wdd)Gy;3 zuV=AzT%DKrzyA8T(@vEqGZHn~L21@(?l-=RJ$zdujJ;lTTT?$fnW2SXP7Cw#Vimd$ ztuit$G59j;WtKn1mVbi%Liaz_#@~&&oI|_EqLdnyXsI0bC%bE}mjT3vVT-HGLY$}8 zOXg&L!7&e!oD0cQI`=x1;mhv*EPvKZV<8(H*2P81@;7K4aIlbWEbZ;*L#L_e%!U%azocimNHU7BX@6AKBAClZAb^j+r%Im%D}u3};|S8tGR z$!YD@B_Tm_E4iO_w^>Tmd5Ly&o14X>?;miV$g9plvL%Jr+y86q`eULl!}xKQUzl_p z2p(_;-9ZAT5I+QJP25h4h+r}^wl1b*rZg=;EgIC6Or1ea&0dGJB+Oh&q{|SlA@dgy zAkY|Vk*L#Xv|G-yrXFga=li}daOa)*7aZUB`@YZn{P;Z2``r6H1hcFD%2Ouqs^BVU z79OF5y9wVkznPy|2Il?KkgWZiVqGXZ!6AS7^$C+j3leG}L4~65!)n2wFc}y9^!be4 z*1@VppUGt{9G17&n$M4INTq5mWckRG8d-LL5@uSD0pv$F%^xs6-`9~z6kq^kG0)C6 z1VCt6cCKuFBL#m27xIxrr&G={fr~qo_PptND-wQ5nY@u^3A+ifWL) zDh?U?SNEO|Mp5bwE1*k=9$0z7&eHV2NC{OjJJ4=;*i1#B1f$b)0TwrTTE2|2^mSy3yD4&)Z z^PpzVUPk?nBvI65N|d#nV8hd4;Jjjo>4WbXDHrFfvBSa2DmmBvT*Kr|$;IH_xWZ#B)^z3nTqxHVP=yTWdmtk*+QAa0NWOXFmoudUu}k;q76UEibFx1AlB%m zvnLYQs+`mdkHT~vCwZV$652OXz55sX&Wh{?cE_c|9t-m-QsJA0SfrO^*Ta-t*I3sW zahiLw<2x!YSogg{03o>LR_7})N4onMyhpLfj2HZ`e%D$~VXbh}(rZd|^5nKtPO}Ol zWVHaZkU9ih-W?N_m{0g8u)!s?mKzXmUaA1@>L;0CWt=oL0U|_``{U#rmz?2Y$=mfk z3Hl<(f(bhL3T(s#(FvLfqjRMVh*{N^G85s+&1?M1-odv9?foT>AbgeQj`L@jBLboZ zho(j<acYB#L8gcr+Y?oAVUt z!fK+GN^NQ&AW7EILzZBypQ5xkT_YO6sK5W##SYU9PRv00`FUr3;}-k2ADc+L1a`BF7(p{6 zQ1Co?Ch<2fQ38=&(8B|iyx4^3V>Pxg+7J(~c=)SQMnSpg;A26PR)D(K{W!bWow#w+ zc00J49y-U5N|9?qwmYo_2EH0ujXX?wKiUbD5z6SWPQQZ2KbAVmRY9xw`RaPvo3nuci+Axf))hxfc$@ilKb_Lm-Zv-;r4BN< z%&0b!8JGEzNOV_T@bXzl8oSC+^iW*+Z)Uoa8 z*r`Q%2deOy(q)&|RTk-A%NqKW)y+{JwY+hO$7VxI%J=6VGKa0lXF6PNG%V5G4W2u^ zkzK;D!jWA5HoKEzUHxr#ko&)Ta(}*d{qoX6Q1ktn;%R$xLQ4OCnZkHwzAd|s`;bjf zrgr<3>F!MbhI{6Bdm6_o%YILdz-J0w%GV4oytp!X+5se2=pO3vKWBFAy5?Jyfaj{0 qTbgHx+w<&C2WN36HQe%Ta>gtDQ@1$hDoxJ0zI`t>wO9FC*8K-Xb)*OY diff --git a/components/Ratings.tsx b/components/Ratings.tsx index f18a0843..64d3d83b 100644 --- a/components/Ratings.tsx +++ b/components/Ratings.tsx @@ -7,6 +7,7 @@ import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search"; import { useJellyseerr } from "@/hooks/useJellyseerr"; import { useQuery } from "@tanstack/react-query"; import { MediaType } from "@/utils/jellyseerr/server/constants/media"; + interface Props extends ViewProps { item?: BaseItemDto | null; } diff --git a/components/series/SeasonPicker.tsx b/components/series/SeasonPicker.tsx index d5fbabe9..26a5f747 100644 --- a/components/series/SeasonPicker.tsx +++ b/components/series/SeasonPicker.tsx @@ -56,6 +56,7 @@ export const SeasonPicker: React.FC = ({ item, initialSeasonIndex }) => { return response.data.Items; }, + staleTime: 60, enabled: !!api && !!user?.Id && !!item.Id, }); diff --git a/components/series/SeriesActions.tsx b/components/series/SeriesActions.tsx new file mode 100644 index 00000000..7e87247f --- /dev/null +++ b/components/series/SeriesActions.tsx @@ -0,0 +1,32 @@ +import { Ionicons } from "@expo/vector-icons"; +import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import { useRouter } from "expo-router"; +import { useCallback, useMemo } from "react"; +import { View, TouchableOpacity, ViewProps } from "react-native"; + +interface Props extends ViewProps { + item: BaseItemDto; +} + +export const SeriesActions = ({ item, ...props }: Props) => { + const router = useRouter(); + + const trailerLink = useMemo(() => item.RemoteTrailers?.[0]?.Url, [item]); + + const openTrailer = useCallback(async () => { + if (!trailerLink) return; + + const encodedTrailerLink = encodeURIComponent(trailerLink); + router.push(`/trailer/page?url=${encodedTrailerLink}`); + }, [router, trailerLink]); + + return ( + + {trailerLink && ( + + + + )} + + ); +}; diff --git a/components/series/SeriesHeader.tsx b/components/series/SeriesHeader.tsx new file mode 100644 index 00000000..3fa5ca67 --- /dev/null +++ b/components/series/SeriesHeader.tsx @@ -0,0 +1,64 @@ +import { View } from "react-native"; +import { Text } from "../common/Text"; +import { Ratings } from "../Ratings"; +import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import { useMemo } from "react"; +import { SeriesActions } from "./SeriesActions"; + +interface Props { + item: BaseItemDto; +} + +export const SeriesHeader = ({ item }: Props) => { + const startYear = useMemo(() => { + if (item?.StartDate) { + return new Date(item.StartDate) + .toLocaleDateString("sv-SE", { + calendar: "gregory", + year: "numeric", + }) + .toString() + .trim(); + } + return item.ProductionYear?.toString().trim(); + }, [item]); + + const endYear = useMemo(() => { + if (item.EndDate) { + return new Date(item.EndDate) + .toLocaleDateString("sv-SE", { + calendar: "gregory", + year: "numeric", + }) + .toString() + .trim(); + } + return ""; + }, [item]); + + const yearString = useMemo(() => { + if (startYear && endYear) { + if (startYear === endYear) return startYear; + return `${startYear} - ${endYear}`; + } + if (startYear) { + return startYear; + } + if (endYear) { + return endYear; + } + return ""; + }, [startYear, endYear]); + + return ( + + {item?.Name} + {yearString} + + + + + {item?.Overview} + + ); +}; diff --git a/package.json b/package.json index a2e3b635..50a4de63 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,8 @@ "react-native-video": "^6.7.0", "react-native-volume-manager": "^1.10.0", "react-native-web": "~0.19.13", + "react-native-webview": "^13.12.5", + "react-native-youtube-iframe": "^2.3.0", "sonner-native": "^0.14.2", "tailwindcss": "3.3.2", "use-debounce": "^10.0.4", From 479a1f037e2c56e8c1b85db6ff9e8e81625939df Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Tue, 31 Dec 2024 16:07:16 +0100 Subject: [PATCH 18/19] fix: padding --- components/series/NextUp.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/series/NextUp.tsx b/components/series/NextUp.tsx index aef8f3f9..95834b9d 100644 --- a/components/series/NextUp.tsx +++ b/components/series/NextUp.tsx @@ -44,9 +44,9 @@ export const NextUp: React.FC<{ seriesId: string }> = ({ seriesId }) => { return ( - Next up + Next up Date: Tue, 31 Dec 2024 17:24:09 +0100 Subject: [PATCH 19/19] feat: explaining new way to enter Beta in README --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f63c47f5..a0843126 100644 --- a/README.md +++ b/README.md @@ -70,11 +70,9 @@ Or download the APKs [here on GitHub](https://github.com/fredrikburmester/stream ### Beta testing -Get the latest updates by using the TestFlight version of the app. +To access the Streamyfin beta, you need to subscribe to the Member tier (or higher) on [Patreon](https://www.patreon.com/streamyfin). This will give you immediate access to the โ ๐Ÿงช-public-beta channel on Discord and i'll know that you have subscribed. This is where i'll post APKs and IPAs. This won't give automatic access to the TestFlight however, so you need to send me a DM with the email you use for Apple so that i can manually add you. - - Get the beta on TestFlight - + **Note**: Everyone who is actively contributing to the source code of Streamyfin will have automatic access to the betas. ## ๐Ÿš€ Getting Started