From b250ab6861442f808b4f1e41d7c18bc1abd5da0a Mon Sep 17 00:00:00 2001
From: Oscar Beaumont
Date: Sun, 2 Oct 2022 01:37:35 +0800
Subject: [PATCH] bundle splitting and performance optimisations
---
.gitignore | 2 +-
apps/mobile/package.json | 2 +-
apps/mobile/pnpm-lock.yaml | Bin 329592 -> 329395 bytes
apps/mobile/rust/src/lib.rs | 5 +-
.../src/components/modals/FileModal.tsx | 6 +-
apps/mobile/src/screens/Overview.tsx | 265 ++++++++++++++----
apps/web/package.json | 2 +
apps/web/vite.config.ts | 15 +-
packages/interface/package.json | 3 +-
packages/interface/src/App.tsx | 8 +
packages/interface/src/AppLayout.tsx | 5 +-
packages/interface/src/AppRouter.tsx | 169 +++++------
packages/interface/src/NotFound.tsx | 2 +-
.../src/components/explorer/FileThumb.tsx | 71 ++---
.../src/components/explorer/Inspector.tsx | 33 +--
.../components/explorer/inspector/Note.tsx | 2 +-
.../src/components/jobs/JobManager.tsx | 6 +-
packages/interface/src/screens/Content.tsx | 4 +-
packages/interface/src/screens/Debug.tsx | 4 +-
.../src/screens/LocationExplorer.tsx | 4 +-
packages/interface/src/screens/Overview.tsx | 4 +-
packages/interface/src/screens/Photos.tsx | 4 +-
.../interface/src/screens/TagExplorer.tsx | 4 +-
.../src/screens/settings/Settings.tsx | 4 +-
pnpm-lock.yaml | Bin 671681 -> 674399 bytes
25 files changed, 405 insertions(+), 219 deletions(-)
diff --git a/.gitignore b/.gitignore
index 73aded57f..c9c309319 100644
--- a/.gitignore
+++ b/.gitignore
@@ -65,4 +65,4 @@ examples/*/*.lock
/core/src/prisma.rs
/sdserver_data
-.spacedrive
+.spacedrive
\ No newline at end of file
diff --git a/apps/mobile/package.json b/apps/mobile/package.json
index 49b065fb3..97ae939b7 100644
--- a/apps/mobile/package.json
+++ b/apps/mobile/package.json
@@ -23,7 +23,7 @@
"@tanstack/react-query": "^4.2.3",
"byte-size": "^8.1.0",
"class-variance-authority": "^0.2.3",
- "date-fns": "^2.29.2",
+ "dayjs": "^1.11.5",
"expo": "~46.0.10",
"expo-linking": "~3.2.2",
"expo-splash-screen": "~0.16.2",
diff --git a/apps/mobile/pnpm-lock.yaml b/apps/mobile/pnpm-lock.yaml
index c2a24bf98284818b212273f2ce5937ce8e1882d8..fd4ed81a51eee68b033ab7ce1b5583d708c68d07 100644
GIT binary patch
delta 185
zcmey-C$hO$WW#D^p31CZD}^{iJwro1)6Kh>r?GK^MIZvxFRo92suxo}5#eZyHumn4MmbWsw$~;uDx0R9cl5VV3J`
z7LZYCkeB9Vkm?@kpBikQYm!@HRFLBm?&DbEXcAHwl9*kVl$7sL7Fv;(YLe;Lp6|o7
cJ>Q4 = Lazy::new(|| Runtime::new().unwrap());
+type NodeType = Lazy, Arc)>>>;
+
#[allow(dead_code)]
-pub(crate) static NODE: Lazy, Arc)>>> =
- Lazy::new(|| Mutex::new(None));
+pub(crate) static NODE: NodeType = Lazy::new(|| Mutex::new(None));
#[allow(dead_code)]
pub(crate) static SUBSCRIPTIONS: Lazy>>> =
diff --git a/apps/mobile/src/components/modals/FileModal.tsx b/apps/mobile/src/components/modals/FileModal.tsx
index f46017c73..35d4434d0 100644
--- a/apps/mobile/src/components/modals/FileModal.tsx
+++ b/apps/mobile/src/components/modals/FileModal.tsx
@@ -1,5 +1,5 @@
import { BottomSheetModal, BottomSheetScrollView } from '@gorhom/bottom-sheet';
-import { format } from 'date-fns';
+import dayjs from 'dayjs';
import React, { useRef } from 'react';
import { Button, Pressable, Text, View } from 'react-native';
import { ChevronLeftIcon } from 'react-native-heroicons/outline';
@@ -98,12 +98,12 @@ export const FileModal = () => {
>
diff --git a/apps/mobile/src/screens/Overview.tsx b/apps/mobile/src/screens/Overview.tsx
index 05666fe54..fffec0059 100644
--- a/apps/mobile/src/screens/Overview.tsx
+++ b/apps/mobile/src/screens/Overview.tsx
@@ -1,65 +1,214 @@
-import React from 'react';
-import { FlatList, View } from 'react-native';
-import Device from '~/components/device/Device';
-import VirtualizedListWrapper from '~/components/layout/VirtualizedListWrapper';
-import OverviewStats from '~/containers/OverviewStats';
-import tw from '~/lib/tailwind';
-import { OverviewStackScreenProps } from '~/navigation/tabs/OverviewStack';
+import { ExclamationCircleIcon, PlusIcon } from '@heroicons/react/24/solid';
+import { useBridgeQuery, useLibraryQuery, usePlatform } from '@sd/client';
+import { Statistics } from '@sd/client';
+import { Button, Input } from '@sd/ui';
+import byteSize from 'byte-size';
+import clsx from 'clsx';
+import { useEffect } from 'react';
+import Skeleton from 'react-loading-skeleton';
+import 'react-loading-skeleton/dist/skeleton.css';
+import create from 'zustand';
-const placeholderOverviewStats = {
- id: 1,
- total_bytes_capacity: '8093333345230',
- preview_media_bytes: '2304387532',
- library_db_size: '83345230',
- total_file_count: 20342345,
- total_bytes_free: '89734502034',
- total_bytes_used: '8093333345230',
- total_unique_bytes: '9347397',
- date_captured: '2020-01-01'
+import { Device } from '../components/device/Device';
+import Dialog from '../components/layout/Dialog';
+import useCounter from '../hooks/useCounter';
+
+interface StatItemProps {
+ title: string;
+ bytes: string;
+ isLoading: boolean;
+}
+
+const StatItemNames: Partial> = {
+ total_bytes_capacity: 'Total capacity',
+ preview_media_bytes: 'Preview media',
+ library_db_size: 'Index size',
+ total_bytes_free: 'Free space'
};
-const placeholderDevices: any = [
- {
- name: "James' iPhone 12",
- size: '47.9GB',
- locations: [],
- type: 'phone'
- },
- {
- name: "James' MacBook Pro",
- size: '1TB',
- locations: [],
- type: 'laptop'
- },
- {
- name: "James' Toaster",
- size: '1PB',
- locations: [],
- type: 'desktop'
- },
- {
- name: 'Spacedrive Server',
- size: '5GB',
- locations: [],
- type: 'server'
- }
-];
+type OverviewStats = Partial>;
+type OverviewState = {
+ overviewStats: OverviewStats;
+ setOverviewStat: (name: keyof OverviewStats, newValue: string) => void;
+ setOverviewStats: (stats: OverviewStats) => void;
+};
+
+export const useOverviewState = create((set) => ({
+ overviewStats: {},
+ setOverviewStat: (name, newValue) =>
+ set((state) => ({
+ ...state,
+ overviewStats: {
+ ...state.overviewStats,
+ [name]: newValue
+ }
+ })),
+ setOverviewStats: (stats) =>
+ set((state) => ({
+ ...state,
+ overviewStats: stats
+ }))
+}));
+
+const StatItem: React.FC = (props) => {
+ const { title, bytes = '0', isLoading } = props;
+
+ // const appProps = useContext(AppPropsContext);
+
+ const size = byteSize(+bytes);
+
+ const count = useCounter({
+ name: title,
+ end: +size.value
+ });
-export default function OverviewScreen({ navigation }: OverviewStackScreenProps<'Overview'>) {
return (
-
-
- {/* Stats */}
-
- {/* Devices */}
- index.toString()}
- renderItem={({ item }) => (
-
- )}
- />
-
-
+
+
{title}
+
+ {isLoading && (
+
+
+
+ )}
+
+ {count}
+ {size.unit}
+
+
+
+ );
+};
+
+export default function OverviewScreen() {
+ const platform = usePlatform();
+ const { data: libraryStatistics, isLoading: isStatisticsLoading } = useLibraryQuery([
+ 'library.getStatistics'
+ ]);
+ const { data: nodeState } = useBridgeQuery(['getNode']);
+
+ const { overviewStats, setOverviewStats } = useOverviewState();
+
+ // get app props from context
+ useEffect(() => {
+ if (platform.demoMode === true) {
+ if (!Object.entries(overviewStats).length)
+ setOverviewStats({
+ total_bytes_capacity: '8093333345230',
+ preview_media_bytes: '2304387532',
+ library_db_size: '83345230',
+ total_file_count: '20342345',
+ total_bytes_free: '89734502034',
+ total_bytes_used: '8093333345230',
+ total_unique_bytes: '9347397'
+ });
+ } else {
+ const newStatistics: OverviewStats = {
+ total_bytes_capacity: '0',
+ preview_media_bytes: '0',
+ library_db_size: '0',
+ total_file_count: '0',
+ total_bytes_free: '0',
+ total_bytes_used: '0',
+ total_unique_bytes: '0'
+ };
+
+ Object.entries((libraryStatistics as Statistics) || {}).forEach(([key, value]) => {
+ newStatistics[key as keyof Statistics] = `${value}`;
+ });
+
+ setOverviewStats(newStatistics);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [platform, libraryStatistics]);
+
+ // useEffect(() => {
+ // setTimeout(() => {
+ // setOverviewStat('total_bytes_capacity', '4093333345230');
+ // }, 2000);
+ // }, [overviewStats]);
+
+ const displayableStatItems = Object.keys(StatItemNames) as unknown as keyof typeof StatItemNames;
+
+ return (
+
+
+ {/* PAGE */}
+
+ {/* STAT HEADER */}
+
+ {/* STAT CONTAINER */}
+
+ {Object.entries(overviewStats).map(([key, value]) => {
+ if (!displayableStatItems.includes(key)) return null;
+
+ return (
+
+ );
+ })}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Note: This is a pre-alpha build of Spacedrive, many features are yet to be
+ functional.
+
+
+
+
);
}
diff --git a/apps/web/package.json b/apps/web/package.json
index df365b134..44d4e572a 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -23,9 +23,11 @@
"@vitejs/plugin-react": "^2.1.0",
"autoprefixer": "^10.4.12",
"postcss": "^8.4.17",
+ "rollup-plugin-visualizer": "^5.8.2",
"tailwind": "^4.0.0",
"typescript": "^4.8.4",
"vite": "^3.1.4",
+ "vite-plugin-html": "^3.2.0",
"vite-plugin-svgr": "^2.2.1",
"vite-plugin-tsconfig-paths": "^1.2.0"
}
diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts
index b22575e2e..affc565fe 100644
--- a/apps/web/vite.config.ts
+++ b/apps/web/vite.config.ts
@@ -1,5 +1,7 @@
import react from '@vitejs/plugin-react';
+import { visualizer } from 'rollup-plugin-visualizer';
import { defineConfig } from 'vite';
+import { createHtmlPlugin } from 'vite-plugin-html';
import svg from 'vite-plugin-svgr';
import tsconfigPaths from 'vite-plugin-tsconfig-paths';
@@ -10,7 +12,18 @@ export default defineConfig({
server: {
port: 8002
},
- plugins: [react(), svg({ svgrOptions: { icon: true } }), tsconfigPaths()],
+ plugins: [
+ react(),
+ svg({ svgrOptions: { icon: true } }),
+ tsconfigPaths(),
+ createHtmlPlugin({
+ minify: true
+ }),
+ visualizer({
+ gzipSize: true,
+ brotliSize: true
+ })
+ ],
root: 'src',
publicDir: '../../packages/interface/src/assets',
define: {
diff --git a/packages/interface/package.json b/packages/interface/package.json
index 370e734a2..df37ad985 100644
--- a/packages/interface/package.json
+++ b/packages/interface/package.json
@@ -37,11 +37,10 @@
"autoprefixer": "^10.4.12",
"byte-size": "^8.1.0",
"clsx": "^1.2.1",
- "date-fns": "^2.29.3",
+ "dayjs": "^1.11.5",
"immer": "^9.0.15",
"jotai": "^1.8.4",
"lodash": "^4.17.21",
- "moment": "^2.29.4",
"phosphor-react": "^1.4.1",
"pretty-bytes": "^6.0.0",
"react": "^18.2.0",
diff --git a/packages/interface/src/App.tsx b/packages/interface/src/App.tsx
index 07a4dedff..84e3fd591 100644
--- a/packages/interface/src/App.tsx
+++ b/packages/interface/src/App.tsx
@@ -2,6 +2,10 @@ import '@fontsource/inter/variable.css';
import { LibraryContextProvider, queryClient } from '@sd/client';
import { QueryClientProvider, defaultContext } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
+import dayjs from 'dayjs';
+import advancedFormat from 'dayjs/plugin/advancedFormat';
+import duration from 'dayjs/plugin/duration';
+import relativeTime from 'dayjs/plugin/relativeTime';
import { ErrorBoundary } from 'react-error-boundary';
import { MemoryRouter, useNavigate } from 'react-router-dom';
@@ -9,6 +13,10 @@ import { AppRouter } from './AppRouter';
import { ErrorFallback } from './ErrorFallback';
import './style.scss';
+dayjs.extend(advancedFormat);
+dayjs.extend(relativeTime);
+dayjs.extend(duration);
+
export default function SpacedriveInterface() {
return (
diff --git a/packages/interface/src/AppLayout.tsx b/packages/interface/src/AppLayout.tsx
index 14a9ab835..263173dae 100644
--- a/packages/interface/src/AppLayout.tsx
+++ b/packages/interface/src/AppLayout.tsx
@@ -1,5 +1,6 @@
import { useCurrentLibrary } from '@sd/client';
import clsx from 'clsx';
+import { Suspense } from 'react';
import { Outlet } from 'react-router-dom';
import { Sidebar } from './components/layout/Sidebar';
@@ -29,7 +30,9 @@ export function AppLayout() {
>
-
+ Loading...}>
+
+
);
diff --git a/packages/interface/src/AppRouter.tsx b/packages/interface/src/AppRouter.tsx
index 363be4cfb..0ec119793 100644
--- a/packages/interface/src/AppRouter.tsx
+++ b/packages/interface/src/AppRouter.tsx
@@ -1,38 +1,43 @@
import { useCurrentLibrary, useInvalidateQuery } from '@sd/client';
+import { Suspense, lazy } from 'react';
import { Route, Routes } from 'react-router-dom';
import { AppLayout } from './AppLayout';
-import { NotFound } from './NotFound';
-import OnboardingScreen from './components/onboarding/Onboarding';
import { useKeybindHandler } from './hooks/useKeyboardHandler';
-import { ContentScreen } from './screens/Content';
-import { DebugScreen } from './screens/Debug';
-import { LocationExplorer } from './screens/LocationExplorer';
-import { OverviewScreen } from './screens/Overview';
-import { PhotosScreen } from './screens/Photos';
import { RedirectPage } from './screens/Redirect';
-import { TagExplorer } from './screens/TagExplorer';
-import { SettingsScreen } from './screens/settings/Settings';
-import AppearanceSettings from './screens/settings/client/AppearanceSettings';
-import ExtensionSettings from './screens/settings/client/ExtensionsSettings';
-import GeneralSettings from './screens/settings/client/GeneralSettings';
-import KeybindingSettings from './screens/settings/client/KeybindingSettings';
-import PrivacySettings from './screens/settings/client/PrivacySettings';
-import AboutSpacedrive from './screens/settings/info/AboutSpacedrive';
-import Changelog from './screens/settings/info/Changelog';
-import Support from './screens/settings/info/Support';
-import ContactsSettings from './screens/settings/library/ContactsSettings';
-import KeysSettings from './screens/settings/library/KeysSetting';
-import LibraryGeneralSettings from './screens/settings/library/LibraryGeneralSettings';
-import LocationSettings from './screens/settings/library/LocationSettings';
-import NodesSettings from './screens/settings/library/NodesSettings';
-import SecuritySettings from './screens/settings/library/SecuritySettings';
-import SharingSettings from './screens/settings/library/SharingSettings';
-import SyncSettings from './screens/settings/library/SyncSettings';
-import TagsSettings from './screens/settings/library/TagsSettings';
-import ExperimentalSettings from './screens/settings/node/ExperimentalSettings';
-import LibrarySettings from './screens/settings/node/LibrariesSettings';
-import P2PSettings from './screens/settings/node/P2PSettings';
+
+const DebugScreen = lazy(() => import('./screens/Debug'));
+const SettingsScreen = lazy(() => import('./screens/settings/Settings'));
+const TagExplorer = lazy(() => import('./screens/TagExplorer'));
+const PhotosScreen = lazy(() => import('./screens/Photos'));
+const OverviewScreen = lazy(() => import('./screens/Overview'));
+const ContentScreen = lazy(() => import('./screens/Content'));
+const LocationExplorer = lazy(() => import('./screens/LocationExplorer'));
+const OnboardingScreen = lazy(() => import('./components/onboarding/Onboarding'));
+const NotFound = lazy(() => import('./NotFound'));
+
+const AppearanceSettings = lazy(() => import('./screens/settings/client/AppearanceSettings'));
+const ExtensionSettings = lazy(() => import('./screens/settings/client/ExtensionsSettings'));
+const GeneralSettings = lazy(() => import('./screens/settings/client/GeneralSettings'));
+const KeybindingSettings = lazy(() => import('./screens/settings/client/KeybindingSettings'));
+const PrivacySettings = lazy(() => import('./screens/settings/client/PrivacySettings'));
+const AboutSpacedrive = lazy(() => import('./screens/settings/info/AboutSpacedrive'));
+const Changelog = lazy(() => import('./screens/settings/info/Changelog'));
+const Support = lazy(() => import('./screens/settings/info/Support'));
+const ContactsSettings = lazy(() => import('./screens/settings/library/ContactsSettings'));
+const KeysSettings = lazy(() => import('./screens/settings/library/KeysSetting'));
+const LibraryGeneralSettings = lazy(
+ () => import('./screens/settings/library/LibraryGeneralSettings')
+);
+const LocationSettings = lazy(() => import('./screens/settings/library/LocationSettings'));
+const NodesSettings = lazy(() => import('./screens/settings/library/NodesSettings'));
+const SecuritySettings = lazy(() => import('./screens/settings/library/SecuritySettings'));
+const SharingSettings = lazy(() => import('./screens/settings/library/SharingSettings'));
+const SyncSettings = lazy(() => import('./screens/settings/library/SyncSettings'));
+const TagsSettings = lazy(() => import('./screens/settings/library/TagsSettings'));
+const ExperimentalSettings = lazy(() => import('./screens/settings/node/ExperimentalSettings'));
+const LibrarySettings = lazy(() => import('./screens/settings/node/LibrariesSettings'));
+const P2PSettings = lazy(() => import('./screens/settings/node/P2PSettings'));
export function AppRouter() {
const { library } = useCurrentLibrary();
@@ -41,56 +46,60 @@ export function AppRouter() {
useInvalidateQuery();
return (
-
- } />
- }>
- {/* As we are caching the libraries in localStore so this *shouldn't* result is visual problems unless something else is wrong */}
- {library === undefined ? (
- Please select or create a library in the sidebar.
- }
- />
- ) : (
- <>
- } />
- } />
- } />
- } />
- } />
- }>
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
-
- } />
- } />
- } />
- >
- )}
-
-
+ Loading...
}>
+
+ } />
+ }>
+ {/* As we are caching the libraries in localStore so this *shouldn't* result is visual problems unless something else is wrong */}
+ {library === undefined ? (
+
+ Please select or create a library in the sidebar.
+
+ }
+ />
+ ) : (
+ <>
+ } />
+ } />
+ } />
+ } />
+ } />
+ }>
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+ } />
+ } />
+ } />
+ >
+ )}
+
+
+
);
}
diff --git a/packages/interface/src/NotFound.tsx b/packages/interface/src/NotFound.tsx
index aeec54c35..ad732dac1 100644
--- a/packages/interface/src/NotFound.tsx
+++ b/packages/interface/src/NotFound.tsx
@@ -1,7 +1,7 @@
import { Button } from '@sd/ui';
import { useNavigate } from 'react-router';
-export function NotFound() {
+export default function NotFound() {
const navigate = useNavigate();
return (
;
+ const Icon = useMemo(() => {
+ const icon = icons[`../../../../assets/icons/${data.extension as any}.svg`];
+ const Icon = icon
+ ? lazy(() => icon().then((v) => ({ default: (v as any).ReactComponent })))
+ : undefined;
+ return Icon;
+ }, [data.extension]);
+
+ if (isPath(data) && data.is_dir) return ;
const cas_id = isObject(data) ? data.cas_id : data.file?.cas_id;
- if (cas_id) {
- // this won't work
- const new_thumbnail = !!getExplorerStore().newThumbnails[cas_id];
+ if (!cas_id) return ;
- const has_thumbnail = isObject(data)
- ? data.has_thumbnail
- : isPath(data)
- ? data.file?.has_thumbnail
- : new_thumbnail;
+ const has_thumbnail = isObject(data)
+ ? data.has_thumbnail
+ : isPath(data)
+ ? data.file?.has_thumbnail
+ : !!store.newThumbnails[cas_id];
- const url = platform.getThumbnailUrlById(cas_id);
-
- if (has_thumbnail && url)
- return (
-
- );
- }
-
- const Icon = icons[data.extension as keyof typeof icons];
+ if (has_thumbnail)
+ return (
+
+ );
return (
);
-};
+}
diff --git a/packages/interface/src/screens/settings/Settings.tsx b/packages/interface/src/screens/settings/Settings.tsx
index 34a095ffa..535338c30 100644
--- a/packages/interface/src/screens/settings/Settings.tsx
+++ b/packages/interface/src/screens/settings/Settings.tsx
@@ -17,7 +17,7 @@ import {
SettingsScreenContainer
} from '../../components/settings/SettingsScreenContainer';
-export const SettingsScreen: React.FC = () => {
+export default function SettingsScreen() {
return (
Client
@@ -100,4 +100,4 @@ export const SettingsScreen: React.FC = () => {
);
-};
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4ceeb59e000e2d3b03cca273fae98ebc9d8ab632..ec41ecff1e6a81a924de5923c99a7b7ab537d30b 100644
GIT binary patch
delta 1881
zcmZ`)Ta4pY8CGVJnPg{n+PyHd?XnfRW|KudF(#IbX8630nYP#s?rJGPTJ
z8QWai3dE`}NJ}amgg}Mzf`HPkRuCkfS|K3;673r=6nI03RxCoiPywFZ6(A&z^uL_{
zJLfBePh!b
zPJN4tI<>ES2iX0zV((*=e7e6w7qNfGtKt
zYj{gkv*U^hd0tLXYp#glis$E>3CgqCzUuOQ-Oif|iP>m*)*xg)&%#Wa2e;YK&eHyB
z_}dqDO<8mfwZYV}Cf=c74-OT
zk7h&XW0DYX1P2rW_7|b+tz1UxkSyNi@uFyW7TLDPx@r$oN>eJcVq@kjox~`YsB0c#
zDl{S&yEVC)91$Z2vyD7o&NvyOHEs#|yx+H|Ip-=e0iO9{^s(iy-Vf(e;Ir2vQ80Nv
zdXsA&PimF9DkDA4<0UeSR67;c$m>-oHGwFt%}rgkR!f$wdcG#*8+nW#9=mom;o*!~
zXe1KDDP1{fny5W44dxn0PDFZ81@G{YtKe<;%vMaY9NC=1Xac-O9b5_DNRAba?LRPDI3~v7}2=?69
zBjLs0y{plopvy;lC<0y|hIf|xaHR24*a&(Uyz$p{h&w^W6?0l?qnX)R4*eUA*
z;nlUP;E|Ws&w;&S_#$}wH<1W<;3Xv&z7bPty)c5l6tcJ`=hOK2?fsE$ttzWnNy^
zne`7Yo;{VWLtB_kL
zpL}C=Ygv45{d@he;Ak>nACE%Zv^8*A^{#1RxI8U1@P;w&W_gnk36d|$WpT>*?0~a7
zbjDLeUbl?7(Y4#DLfJ~vgQ>=(iG;><>agj|uvlk~gYpEUee2#^>w!g=_uh)V
zaWi_FG#tn^_`!QIjKkXHoSYv}qc)r!kECbYwrBh^wa?}%wf)i7evfyqz;gxELty=eJxL)*}K0)%lyB3drD#=j{FCCDvPJ*Q8JD{+oF|p}3bD4OiZ$>j6u;?3~}?ePYTK+FWh%-iD)SjscDm(OHbD7?LW9m@`@C&T{k6
zFY_#RG;sAZFi1`>vY4K5j$LBAeh~XVk?njL?8}nc15UF8F~|0R)0`W$wmbabln(*`
D$E|ak