linked-data-browser 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +13 -0
- package/.ldo/profile.context.ts +459 -0
- package/.ldo/profile.schema.ts +751 -0
- package/.ldo/profile.shapeTypes.ts +71 -0
- package/.ldo/profile.typings.ts +295 -0
- package/.prettierignore +6 -0
- package/.prettierrc +10 -0
- package/.shapes/profile.shex +121 -0
- package/README.md +3 -0
- package/app/index.tsx +25 -0
- package/app.json +37 -0
- package/assets/images/adaptive-icon.png +0 -0
- package/assets/images/favicon.png +0 -0
- package/assets/images/icon.png +0 -0
- package/assets/images/splash.png +0 -0
- package/babel.config.js +6 -0
- package/components/DataBrowser.tsx +57 -0
- package/components/ResourceView.tsx +14 -0
- package/components/TargetResourceProvider.tsx +128 -0
- package/components/ThemeProvider.tsx +123 -0
- package/components/nav/DialogProvider.tsx +140 -0
- package/components/nav/Layout.tsx +118 -0
- package/components/nav/header/AddressBox.tsx +126 -0
- package/components/nav/header/AvatarMenu.tsx +62 -0
- package/components/nav/header/Header.tsx +28 -0
- package/components/nav/header/SignInMenu.tsx +126 -0
- package/components/nav/header/ThemeToggleMenu.tsx +34 -0
- package/components/nav/header/ViewMenu.tsx +88 -0
- package/components/nav/useValidView.tsx +51 -0
- package/components/nav/utilityResourceViews/ErrorMessageResourceView.tsx +26 -0
- package/components/ui/avatar.tsx +53 -0
- package/components/ui/button.tsx +88 -0
- package/components/ui/card.tsx +101 -0
- package/components/ui/dialog.tsx +159 -0
- package/components/ui/dropdown-menu.tsx +275 -0
- package/components/ui/input.tsx +25 -0
- package/components/ui/label.tsx +34 -0
- package/components/ui/navigation-menu.tsx +200 -0
- package/components/ui/popover.tsx +45 -0
- package/components/ui/progress.tsx +79 -0
- package/components/ui/radio-group.tsx +59 -0
- package/components/ui/separator.tsx +27 -0
- package/components/ui/switch.tsx +105 -0
- package/components/ui/text.tsx +30 -0
- package/components/ui/tooltip.tsx +46 -0
- package/components.json +7 -0
- package/global.css +61 -0
- package/index.js +12 -0
- package/lib/android-navigation-bar.ts +11 -0
- package/lib/constants.ts +18 -0
- package/lib/icons/ArrowRight.tsx +4 -0
- package/lib/icons/Check.tsx +4 -0
- package/lib/icons/ChevronDown.tsx +4 -0
- package/lib/icons/ChevronRight.tsx +4 -0
- package/lib/icons/ChevronUp.tsx +4 -0
- package/lib/icons/ChevronsRight.tsx +4 -0
- package/lib/icons/CircleSlash.tsx +4 -0
- package/lib/icons/CircleX.tsx +4 -0
- package/lib/icons/Code.tsx +4 -0
- package/lib/icons/EllipsisVertical.tsx +4 -0
- package/lib/icons/EyeOff.tsx +4 -0
- package/lib/icons/File.tsx +4 -0
- package/lib/icons/Folder.tsx +4 -0
- package/lib/icons/Folders.tsx +4 -0
- package/lib/icons/Info.tsx +4 -0
- package/lib/icons/MonitorSmartphone.tsx +4 -0
- package/lib/icons/MoonStar.tsx +4 -0
- package/lib/icons/OctagonX.tsx +4 -0
- package/lib/icons/RefreshCw.tsx +4 -0
- package/lib/icons/ShieldX.tsx +4 -0
- package/lib/icons/Sun.tsx +4 -0
- package/lib/icons/TextCursorInput.tsx +4 -0
- package/lib/icons/Trash.tsx +4 -0
- package/lib/icons/ViewIcon.tsx +4 -0
- package/lib/icons/X.tsx +4 -0
- package/lib/icons/iconWithClassName.ts +14 -0
- package/lib/utils.ts +6 -0
- package/metro.config.js +6 -0
- package/nativewind-env.d.ts +1 -0
- package/package.json +89 -0
- package/resourceViews/Container/ContainerConfig.tsx +13 -0
- package/resourceViews/Container/ContainerView.tsx +148 -0
- package/resourceViews/RawCode/RawCodeConfig.tsx +11 -0
- package/resourceViews/RawCode/RawCodeEditor.tsx +37 -0
- package/resourceViews/RawCode/RawCodeView.tsx +67 -0
- package/scripts/adjust-server-paths.js +37 -0
- package/scripts/adjust-standalone-paths.js +28 -0
- package/tailwind.config.js +69 -0
- package/test-server/server-config.json +75 -0
- package/test-server/solid-css-seed.json +11 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
FunctionComponent,
|
|
4
|
+
PropsWithChildren,
|
|
5
|
+
createContext,
|
|
6
|
+
useContext,
|
|
7
|
+
} from 'react';
|
|
8
|
+
import { SolidLeaf, SolidContainer } from '@ldo/connected-solid';
|
|
9
|
+
import { InvalidIdentifierResource } from '@ldo/connected';
|
|
10
|
+
import { useDataBrowserConfig } from './DataBrowser';
|
|
11
|
+
import { useResource } from '@ldo/solid-react';
|
|
12
|
+
import { Platform } from 'react-native';
|
|
13
|
+
|
|
14
|
+
interface UseTargetResourceContext {
|
|
15
|
+
targetUri?: string;
|
|
16
|
+
targetResource?: SolidLeaf | SolidContainer | InvalidIdentifierResource;
|
|
17
|
+
refresh: () => void;
|
|
18
|
+
navigateTo: (uri: string) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// @ts-ignore The default value will be filled in upon mount
|
|
22
|
+
const TargetResourceContext = createContext<UseTargetResourceContext>({});
|
|
23
|
+
|
|
24
|
+
export function useTargetResource() {
|
|
25
|
+
return useContext(TargetResourceContext);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const TargetResourceProvider: FunctionComponent<PropsWithChildren> = ({
|
|
29
|
+
children,
|
|
30
|
+
}) => {
|
|
31
|
+
const { mode, origin } = useDataBrowserConfig();
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* URL Management
|
|
35
|
+
*/
|
|
36
|
+
const [currentUrl, setCurrentUrl] = useState<URL>(() => {
|
|
37
|
+
return Platform.OS === 'web' ? new URL(window.location.href) : new URL('');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Sync with addressbar
|
|
41
|
+
const handleUrlChange = useCallback(() => {
|
|
42
|
+
setCurrentUrl(new URL(window.location.href));
|
|
43
|
+
}, []);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (Platform.OS === 'web') {
|
|
46
|
+
// 1. Listen for 'popstate' events (triggered by browser back/forward buttons, or history.go())
|
|
47
|
+
window.addEventListener('popstate', handleUrlChange);
|
|
48
|
+
|
|
49
|
+
// 2. Patch history methods to also trigger our handler
|
|
50
|
+
// This ensures our hook reacts when the app itself changes the URL programmatically.
|
|
51
|
+
const originalPushState = history.pushState;
|
|
52
|
+
const originalReplaceState = history.replaceState;
|
|
53
|
+
|
|
54
|
+
history.pushState = function (...args) {
|
|
55
|
+
originalPushState.apply(history, args);
|
|
56
|
+
handleUrlChange(); // Trigger update after pushState
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
history.replaceState = function (...args) {
|
|
60
|
+
originalReplaceState.apply(history, args);
|
|
61
|
+
handleUrlChange(); // Trigger update after replaceState
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Cleanup: Remove event listener and restore original history methods on unmount
|
|
65
|
+
return () => {
|
|
66
|
+
window.removeEventListener('popstate', handleUrlChange);
|
|
67
|
+
history.pushState = originalPushState;
|
|
68
|
+
history.replaceState = originalReplaceState;
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}, [handleUrlChange]);
|
|
72
|
+
|
|
73
|
+
const navigateTo = useCallback(
|
|
74
|
+
(newRoute: string) => {
|
|
75
|
+
const newUrl = new URL(newRoute);
|
|
76
|
+
let finalUrl: string;
|
|
77
|
+
if (mode === 'server-ui' && newUrl.origin === origin) {
|
|
78
|
+
finalUrl = newRoute;
|
|
79
|
+
} else {
|
|
80
|
+
finalUrl = `${origin}?uri=${encodeURIComponent(newRoute)}`;
|
|
81
|
+
}
|
|
82
|
+
setCurrentUrl(new URL(finalUrl));
|
|
83
|
+
if (Platform.OS === 'web') {
|
|
84
|
+
window.history.pushState(null, '', finalUrl);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
[origin, mode],
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Target Resource Management
|
|
92
|
+
*/
|
|
93
|
+
const targetUri = useMemo<string | undefined>(() => {
|
|
94
|
+
const searchParams = currentUrl.searchParams.get('uri');
|
|
95
|
+
if (searchParams) return searchParams;
|
|
96
|
+
// If we're a standalone app and the uri isn't provided in the search params, it's undefined
|
|
97
|
+
if (mode === 'standalone-app') return undefined;
|
|
98
|
+
// Must be in web if this is server-hosted
|
|
99
|
+
const curOrigin = currentUrl.origin;
|
|
100
|
+
const curPathname = currentUrl.pathname;
|
|
101
|
+
const curHash = currentUrl.hash;
|
|
102
|
+
return `${curOrigin}${curPathname}${curHash}`;
|
|
103
|
+
}, [currentUrl, mode]);
|
|
104
|
+
|
|
105
|
+
const targetResource = useResource(targetUri);
|
|
106
|
+
|
|
107
|
+
const refresh = useCallback(async () => {
|
|
108
|
+
if (targetResource) {
|
|
109
|
+
await targetResource?.read();
|
|
110
|
+
}
|
|
111
|
+
}, [targetResource]);
|
|
112
|
+
|
|
113
|
+
const context = useMemo(
|
|
114
|
+
() => ({
|
|
115
|
+
targetUri,
|
|
116
|
+
targetResource,
|
|
117
|
+
refresh,
|
|
118
|
+
navigateTo,
|
|
119
|
+
}),
|
|
120
|
+
[targetUri, targetResource, refresh, navigateTo],
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<TargetResourceContext.Provider value={context}>
|
|
125
|
+
{children}
|
|
126
|
+
</TargetResourceContext.Provider>
|
|
127
|
+
);
|
|
128
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import '~/global.css';
|
|
3
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
4
|
+
import {
|
|
5
|
+
FunctionComponent,
|
|
6
|
+
PropsWithChildren,
|
|
7
|
+
createContext,
|
|
8
|
+
useContext,
|
|
9
|
+
useMemo,
|
|
10
|
+
useState,
|
|
11
|
+
} from 'react';
|
|
12
|
+
import { Appearance, ColorSchemeName, Platform } from 'react-native';
|
|
13
|
+
import {
|
|
14
|
+
ThemeProvider as ApplicationThemeProvider,
|
|
15
|
+
DarkTheme,
|
|
16
|
+
DefaultTheme,
|
|
17
|
+
Theme,
|
|
18
|
+
} from '@react-navigation/native';
|
|
19
|
+
import { NAV_THEME } from '~/lib/constants';
|
|
20
|
+
import { useColorScheme } from 'nativewind';
|
|
21
|
+
import { setAndroidNavigationBar } from '~/lib/android-navigation-bar';
|
|
22
|
+
|
|
23
|
+
const COLOR_SCHEME_KEY = 'colorScheme';
|
|
24
|
+
|
|
25
|
+
const LIGHT_THEME: Theme = {
|
|
26
|
+
...DefaultTheme,
|
|
27
|
+
colors: NAV_THEME.light,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const DARK_THEME: Theme = {
|
|
31
|
+
...DarkTheme,
|
|
32
|
+
colors: NAV_THEME.dark,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
interface UseThemeChangeContext {
|
|
36
|
+
setColorScheme: (scheme: NonNullable<ColorSchemeName>) => void;
|
|
37
|
+
loadingColorScheme: boolean;
|
|
38
|
+
colorScheme: NonNullable<ColorSchemeName>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const ThemeProviderContext = createContext<UseThemeChangeContext>({
|
|
42
|
+
loadingColorScheme: true,
|
|
43
|
+
setColorScheme: () => {},
|
|
44
|
+
colorScheme: 'light',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export function useThemeChange() {
|
|
48
|
+
return useContext(ThemeProviderContext);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const usePlatformSpecificSetup = Platform.select({
|
|
52
|
+
web: useSetWebBackgroundClassName,
|
|
53
|
+
android: useSetAndroidNavigationBar,
|
|
54
|
+
default: noop,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export const ThemeProvider: FunctionComponent<PropsWithChildren> = ({
|
|
58
|
+
children,
|
|
59
|
+
}) => {
|
|
60
|
+
const { colorScheme, setColorScheme } = useColorScheme();
|
|
61
|
+
const [loadingColorScheme, setLoadingColorScheme] = useState(true);
|
|
62
|
+
|
|
63
|
+
usePlatformSpecificSetup();
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const lookupCurColorScheme = async () => {
|
|
67
|
+
setColorScheme(Appearance.getColorScheme() ?? 'system');
|
|
68
|
+
const storedColorSchemeName: ColorSchemeName =
|
|
69
|
+
((await AsyncStorage.getItem(COLOR_SCHEME_KEY)) as ColorSchemeName) ||
|
|
70
|
+
Appearance.getColorScheme();
|
|
71
|
+
if (storedColorSchemeName) {
|
|
72
|
+
setColorScheme(storedColorSchemeName);
|
|
73
|
+
}
|
|
74
|
+
setLoadingColorScheme(false);
|
|
75
|
+
};
|
|
76
|
+
lookupCurColorScheme();
|
|
77
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
78
|
+
}, []);
|
|
79
|
+
|
|
80
|
+
const context = useMemo(
|
|
81
|
+
() => ({
|
|
82
|
+
setColorScheme: async (newColorScheme: NonNullable<ColorSchemeName>) => {
|
|
83
|
+
setColorScheme(newColorScheme);
|
|
84
|
+
await AsyncStorage.setItem(COLOR_SCHEME_KEY, newColorScheme);
|
|
85
|
+
},
|
|
86
|
+
loadingColorScheme,
|
|
87
|
+
colorScheme: colorScheme ?? 'light',
|
|
88
|
+
}),
|
|
89
|
+
[loadingColorScheme, colorScheme, setColorScheme],
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<>
|
|
94
|
+
<ApplicationThemeProvider
|
|
95
|
+
value={context.colorScheme === 'light' ? LIGHT_THEME : DARK_THEME}
|
|
96
|
+
>
|
|
97
|
+
<ThemeProviderContext.Provider value={context}>
|
|
98
|
+
{children}
|
|
99
|
+
</ThemeProviderContext.Provider>
|
|
100
|
+
</ApplicationThemeProvider>
|
|
101
|
+
</>
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const useIsomorphicLayoutEffect =
|
|
106
|
+
Platform.OS === 'web' && typeof window === 'undefined'
|
|
107
|
+
? React.useEffect
|
|
108
|
+
: React.useLayoutEffect;
|
|
109
|
+
|
|
110
|
+
function useSetWebBackgroundClassName() {
|
|
111
|
+
useIsomorphicLayoutEffect(() => {
|
|
112
|
+
// Adds the background color to the html element to prevent white background on overscroll.
|
|
113
|
+
document.documentElement.classList.add('bg-background');
|
|
114
|
+
}, []);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function useSetAndroidNavigationBar() {
|
|
118
|
+
React.useLayoutEffect(() => {
|
|
119
|
+
setAndroidNavigationBar(Appearance.getColorScheme() ?? 'light');
|
|
120
|
+
}, []);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function noop() {}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// components/DialogProvider.tsx
|
|
2
|
+
import React, {
|
|
3
|
+
createContext,
|
|
4
|
+
useContext,
|
|
5
|
+
useState,
|
|
6
|
+
useRef,
|
|
7
|
+
useCallback,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import {
|
|
10
|
+
Dialog,
|
|
11
|
+
DialogContent,
|
|
12
|
+
DialogHeader,
|
|
13
|
+
DialogFooter,
|
|
14
|
+
DialogTitle,
|
|
15
|
+
DialogDescription,
|
|
16
|
+
DialogClose,
|
|
17
|
+
} from '~/components/ui/dialog';
|
|
18
|
+
import { TextInput, View } from 'react-native';
|
|
19
|
+
import { Button } from '~/components/ui/button';
|
|
20
|
+
import { Text } from '../ui/text';
|
|
21
|
+
|
|
22
|
+
type DialogOptions =
|
|
23
|
+
| { type: 'confirm'; title: string; message?: string }
|
|
24
|
+
| { type: 'prompt'; title: string; message?: string; defaultValue?: string };
|
|
25
|
+
|
|
26
|
+
type DialogContextType = {
|
|
27
|
+
prompt: (
|
|
28
|
+
title: string,
|
|
29
|
+
message?: string,
|
|
30
|
+
defaultValue?: string,
|
|
31
|
+
) => Promise<null | string>;
|
|
32
|
+
confirm: (title: string, message?: string) => Promise<boolean>;
|
|
33
|
+
showDialog: (options: DialogOptions) => Promise<boolean | string>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const DialogContext = createContext<DialogContextType | null>(null);
|
|
37
|
+
|
|
38
|
+
export const useDialog = () => {
|
|
39
|
+
const ctx = useContext(DialogContext);
|
|
40
|
+
if (!ctx) throw new Error('useDialog must be used within DialogProvider');
|
|
41
|
+
return ctx;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const DialogProvider: React.FC<{ children: React.ReactNode }> = ({
|
|
45
|
+
children,
|
|
46
|
+
}) => {
|
|
47
|
+
const [visible, setVisible] = useState(false);
|
|
48
|
+
const [options, setOptions] = useState<DialogOptions | null>(null);
|
|
49
|
+
const [inputValue, setInputValue] = useState('');
|
|
50
|
+
const resolver = useRef<(val: boolean | string) => void>(() => {});
|
|
51
|
+
|
|
52
|
+
const showDialog = useCallback(
|
|
53
|
+
(opts: DialogOptions): Promise<boolean | string> => {
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
resolver.current = resolve;
|
|
56
|
+
setOptions(opts);
|
|
57
|
+
setInputValue(opts.type === 'prompt' ? (opts.defaultValue ?? '') : '');
|
|
58
|
+
setVisible(true);
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
[],
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const prompt = useCallback(
|
|
65
|
+
async (
|
|
66
|
+
title: string,
|
|
67
|
+
message?: string,
|
|
68
|
+
defaultValue?: string,
|
|
69
|
+
): Promise<null | string> => {
|
|
70
|
+
return showDialog({
|
|
71
|
+
type: 'prompt',
|
|
72
|
+
title,
|
|
73
|
+
message,
|
|
74
|
+
defaultValue,
|
|
75
|
+
}) as Promise<string | null>;
|
|
76
|
+
},
|
|
77
|
+
[showDialog],
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const confirm = useCallback(
|
|
81
|
+
async (title: string, message?: string): Promise<boolean> => {
|
|
82
|
+
return showDialog({
|
|
83
|
+
type: 'confirm',
|
|
84
|
+
title,
|
|
85
|
+
message,
|
|
86
|
+
}) as Promise<boolean>;
|
|
87
|
+
},
|
|
88
|
+
[showDialog],
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const handleCancel = () => {
|
|
92
|
+
setVisible(false);
|
|
93
|
+
resolver.current?.(options?.type === 'confirm' ? false : '');
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const handleConfirm = () => {
|
|
97
|
+
setVisible(false);
|
|
98
|
+
resolver.current?.(options?.type === 'confirm' ? true : inputValue);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<DialogContext.Provider value={{ showDialog, prompt, confirm }}>
|
|
103
|
+
{children}
|
|
104
|
+
<Dialog open={visible} onOpenChange={setVisible}>
|
|
105
|
+
<DialogContent>
|
|
106
|
+
<DialogHeader>
|
|
107
|
+
<DialogTitle>{options?.title}</DialogTitle>
|
|
108
|
+
{options?.message && (
|
|
109
|
+
<DialogDescription>{options.message}</DialogDescription>
|
|
110
|
+
)}
|
|
111
|
+
</DialogHeader>
|
|
112
|
+
|
|
113
|
+
{options?.type === 'prompt' && (
|
|
114
|
+
<View className="my-2">
|
|
115
|
+
<TextInput
|
|
116
|
+
value={inputValue}
|
|
117
|
+
onChangeText={setInputValue}
|
|
118
|
+
placeholder="Enter text..."
|
|
119
|
+
className="border p-2 rounded"
|
|
120
|
+
autoFocus
|
|
121
|
+
onSubmitEditing={handleConfirm}
|
|
122
|
+
/>
|
|
123
|
+
</View>
|
|
124
|
+
)}
|
|
125
|
+
|
|
126
|
+
<DialogFooter>
|
|
127
|
+
<DialogClose asChild>
|
|
128
|
+
<Button variant="ghost" onPress={handleCancel}>
|
|
129
|
+
<Text>Cancel</Text>
|
|
130
|
+
</Button>
|
|
131
|
+
</DialogClose>
|
|
132
|
+
<Button onPress={handleConfirm}>
|
|
133
|
+
<Text>Ok</Text>
|
|
134
|
+
</Button>
|
|
135
|
+
</DialogFooter>
|
|
136
|
+
</DialogContent>
|
|
137
|
+
</Dialog>
|
|
138
|
+
</DialogContext.Provider>
|
|
139
|
+
);
|
|
140
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
createContext,
|
|
3
|
+
Dispatch,
|
|
4
|
+
FunctionComponent,
|
|
5
|
+
SetStateAction,
|
|
6
|
+
} from 'react';
|
|
7
|
+
import { useTargetResource } from '../TargetResourceProvider';
|
|
8
|
+
import { ResourceViewConfig } from '../ResourceView';
|
|
9
|
+
import { ErrorMessageResourceView } from './utilityResourceViews/ErrorMessageResourceView';
|
|
10
|
+
import { TextCursorInput } from '~/lib/icons/TextCursorInput';
|
|
11
|
+
import { CircleSlash } from '~/lib/icons/CircleSlash';
|
|
12
|
+
import { OctagonX } from '~/lib/icons/OctagonX';
|
|
13
|
+
import { ShieldX } from '~/lib/icons/ShieldX';
|
|
14
|
+
import { CircleX } from '~/lib/icons/CircleX';
|
|
15
|
+
import { Header } from './header/Header';
|
|
16
|
+
import { View } from 'react-native';
|
|
17
|
+
import { useValidView, ValidViewProvider } from './useValidView';
|
|
18
|
+
import { DialogProvider } from './DialogProvider';
|
|
19
|
+
|
|
20
|
+
export const ValidViewContext = createContext<{
|
|
21
|
+
validViews: ResourceViewConfig[];
|
|
22
|
+
curViewConfig: ResourceViewConfig;
|
|
23
|
+
setCurViewConfig: Dispatch<SetStateAction<ResourceViewConfig>>;
|
|
24
|
+
// @ts-ignore This will be filled in later
|
|
25
|
+
}>({});
|
|
26
|
+
|
|
27
|
+
export const Layout: FunctionComponent = () => {
|
|
28
|
+
return (
|
|
29
|
+
<DialogProvider>
|
|
30
|
+
<ValidViewProvider>
|
|
31
|
+
<Header />
|
|
32
|
+
<View className="flex-1 z-0">
|
|
33
|
+
<RenderView />
|
|
34
|
+
</View>
|
|
35
|
+
</ValidViewProvider>
|
|
36
|
+
</DialogProvider>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* =============================================================================
|
|
42
|
+
* Render View
|
|
43
|
+
* =============================================================================
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
export const RenderView: FunctionComponent = () => {
|
|
47
|
+
const { targetUri, targetResource } = useTargetResource();
|
|
48
|
+
|
|
49
|
+
const { curViewConfig } = useValidView();
|
|
50
|
+
|
|
51
|
+
// Handle Edge cases
|
|
52
|
+
if (!targetResource || !targetUri) {
|
|
53
|
+
return (
|
|
54
|
+
<ErrorMessageResourceView
|
|
55
|
+
icon={TextCursorInput}
|
|
56
|
+
message="Enter a URI in the address bar to view a resource."
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
} else if (targetResource.type === 'InvalidIdentifierResouce') {
|
|
60
|
+
return (
|
|
61
|
+
<ErrorMessageResourceView
|
|
62
|
+
icon={CircleSlash}
|
|
63
|
+
message={`${targetResource.uri} is an invalid URI.`}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
} else if (targetResource?.isDoingInitialFetch()) {
|
|
67
|
+
return <></>;
|
|
68
|
+
} else if (targetResource?.isAbsent()) {
|
|
69
|
+
return (
|
|
70
|
+
<ErrorMessageResourceView
|
|
71
|
+
icon={CircleSlash}
|
|
72
|
+
message={`${targetResource.uri} either doesn't exist or you don't have read access to it.`}
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
} else if (targetResource?.status.isError) {
|
|
76
|
+
switch (targetResource.status.type) {
|
|
77
|
+
case 'noncompliantPodError':
|
|
78
|
+
return (
|
|
79
|
+
<ErrorMessageResourceView
|
|
80
|
+
icon={OctagonX}
|
|
81
|
+
message={`${targetResource.uri} returned a response that is not compliant with the Linked Web Storage specification: ${targetResource.status.message}`}
|
|
82
|
+
/>
|
|
83
|
+
);
|
|
84
|
+
case 'serverError':
|
|
85
|
+
return (
|
|
86
|
+
<ErrorMessageResourceView
|
|
87
|
+
icon={OctagonX}
|
|
88
|
+
message={`${targetResource.uri} encountered an internal server error: ${targetResource.status.message}`}
|
|
89
|
+
/>
|
|
90
|
+
);
|
|
91
|
+
case 'unauthenticatedError':
|
|
92
|
+
return (
|
|
93
|
+
<ErrorMessageResourceView
|
|
94
|
+
icon={ShieldX}
|
|
95
|
+
message={`${targetResource.uri} requires you to log in to view.`}
|
|
96
|
+
/>
|
|
97
|
+
);
|
|
98
|
+
case 'unauthorizedError':
|
|
99
|
+
return (
|
|
100
|
+
<ErrorMessageResourceView
|
|
101
|
+
icon={ShieldX}
|
|
102
|
+
message={`You don't have access to ${targetResource.uri}.`}
|
|
103
|
+
/>
|
|
104
|
+
);
|
|
105
|
+
case 'unexpectedHttpError':
|
|
106
|
+
case 'unexpectedResourceError':
|
|
107
|
+
default:
|
|
108
|
+
return (
|
|
109
|
+
<ErrorMessageResourceView
|
|
110
|
+
icon={CircleX}
|
|
111
|
+
message={`An unexpected error occurred: ${targetResource.status.message}.`}
|
|
112
|
+
/>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const CurView = curViewConfig.view;
|
|
117
|
+
return <CurView />;
|
|
118
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import React, { useEffect, useMemo } from 'react';
|
|
2
|
+
import { FunctionComponent, useState } from 'react';
|
|
3
|
+
import { TouchableOpacity, View } from 'react-native';
|
|
4
|
+
import { Input } from '~/components/ui/input';
|
|
5
|
+
import { ChevronRight } from '~/lib/icons/ChevronRight';
|
|
6
|
+
import { ChevronsRight } from '~/lib/icons/ChevronsRight';
|
|
7
|
+
import { TextCursorInput } from '~/lib/icons/TextCursorInput';
|
|
8
|
+
import { RefreshCw } from '~/lib/icons/RefreshCw';
|
|
9
|
+
import { ArrowRight } from '~/lib/icons/ArrowRight';
|
|
10
|
+
import { Button } from '~/components/ui/button';
|
|
11
|
+
import { Text } from '../../ui/text';
|
|
12
|
+
import { useTargetResource } from '../../TargetResourceProvider';
|
|
13
|
+
|
|
14
|
+
export const AddressBox: FunctionComponent = () => {
|
|
15
|
+
const [isTextMode, setIsTextMode] = useState(false);
|
|
16
|
+
const { targetUri, refresh, navigateTo, targetResource } =
|
|
17
|
+
useTargetResource();
|
|
18
|
+
|
|
19
|
+
const [textBoxValue, setTextBoxValue] = useState(targetUri ?? '');
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
setTextBoxValue(targetUri ?? '');
|
|
22
|
+
}, [targetUri]);
|
|
23
|
+
|
|
24
|
+
const breadcrumbInfo = useMemo<{ name: string; uri: string }[]>(() => {
|
|
25
|
+
if (!targetUri) return [];
|
|
26
|
+
try {
|
|
27
|
+
const uri = new URL(targetUri);
|
|
28
|
+
const pathSplit = uri.pathname.split('/').filter((val) => val !== '');
|
|
29
|
+
const endsInSlash = uri.pathname.endsWith('/');
|
|
30
|
+
|
|
31
|
+
let curUri = `${uri.origin}/`;
|
|
32
|
+
const info: { name: string; uri: string }[] = [
|
|
33
|
+
{
|
|
34
|
+
name: uri.host,
|
|
35
|
+
uri: curUri,
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
pathSplit.forEach((name, index) => {
|
|
39
|
+
curUri +=
|
|
40
|
+
index === pathSplit.length - 1 && !endsInSlash ? name : `${name}/`;
|
|
41
|
+
info.push({
|
|
42
|
+
uri: curUri,
|
|
43
|
+
name,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return info;
|
|
48
|
+
} catch {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
}, [targetUri]);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<View className="flex-1">
|
|
55
|
+
<Input
|
|
56
|
+
className="flex-1 bg-secondary web:py-2.5 border-none pl-10 pr-10 h-[40px] text-sm web:focus-visible:ring-0 web:focus-visible:ring-transparent web:focus-visible:ring-offset-0 web:focus:outline-none web:outline-none"
|
|
57
|
+
onFocus={() => setIsTextMode(true)}
|
|
58
|
+
onBlur={() => setTimeout(() => setIsTextMode(false), 100)}
|
|
59
|
+
onChangeText={setTextBoxValue}
|
|
60
|
+
value={isTextMode ? textBoxValue : ''}
|
|
61
|
+
onSubmitEditing={() => {
|
|
62
|
+
if (textBoxValue) {
|
|
63
|
+
navigateTo(textBoxValue);
|
|
64
|
+
}
|
|
65
|
+
}}
|
|
66
|
+
/>
|
|
67
|
+
<Button
|
|
68
|
+
variant="secondary"
|
|
69
|
+
className="absolute left-0 w-10 h-10"
|
|
70
|
+
onPress={() => setIsTextMode((val) => !val)}
|
|
71
|
+
>
|
|
72
|
+
<Text>
|
|
73
|
+
{isTextMode ? (
|
|
74
|
+
<ChevronsRight size={20} />
|
|
75
|
+
) : (
|
|
76
|
+
<TextCursorInput size={20} />
|
|
77
|
+
)}
|
|
78
|
+
</Text>
|
|
79
|
+
</Button>
|
|
80
|
+
{(() => {
|
|
81
|
+
const shouldRefresh = targetUri === textBoxValue || !isTextMode;
|
|
82
|
+
return (
|
|
83
|
+
<Button
|
|
84
|
+
variant="secondary"
|
|
85
|
+
className="absolute right-0 w-10 h-10"
|
|
86
|
+
onPressIn={() => {
|
|
87
|
+
if (shouldRefresh) {
|
|
88
|
+
refresh();
|
|
89
|
+
} else if (textBoxValue) {
|
|
90
|
+
navigateTo(textBoxValue);
|
|
91
|
+
}
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
<Text className={targetResource?.isLoading() ? 'animate-spin' : ''}>
|
|
95
|
+
{shouldRefresh || targetResource?.isLoading() ? (
|
|
96
|
+
<RefreshCw size={20} />
|
|
97
|
+
) : (
|
|
98
|
+
<ArrowRight size={20} />
|
|
99
|
+
)}
|
|
100
|
+
</Text>
|
|
101
|
+
</Button>
|
|
102
|
+
);
|
|
103
|
+
})()}
|
|
104
|
+
<View
|
|
105
|
+
className="absolute top-0 left-0 right-0 bottom-0 flex-row-reverse items-center ml-10 mr-10 overflow-x-auto scrollbar-hide [direction:rtl]"
|
|
106
|
+
pointerEvents="none"
|
|
107
|
+
>
|
|
108
|
+
{!isTextMode &&
|
|
109
|
+
breadcrumbInfo.map((item, index) => (
|
|
110
|
+
<View className="flex-row" key={item.uri}>
|
|
111
|
+
{index !== breadcrumbInfo.length - 1 ? (
|
|
112
|
+
<ChevronRight className="w-4 h-4 mr-0.5 mt-0.5 text-gray-500" />
|
|
113
|
+
) : (
|
|
114
|
+
<View className="w-2" />
|
|
115
|
+
)}
|
|
116
|
+
<TouchableOpacity onPress={() => navigateTo(item.uri)}>
|
|
117
|
+
<View pointerEvents="auto">
|
|
118
|
+
<Text className="mr-0.5 underline text-sm">{item.name}</Text>
|
|
119
|
+
</View>
|
|
120
|
+
</TouchableOpacity>
|
|
121
|
+
</View>
|
|
122
|
+
))}
|
|
123
|
+
</View>
|
|
124
|
+
</View>
|
|
125
|
+
);
|
|
126
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { FunctionComponent } from 'react';
|
|
4
|
+
import { StyleSheet } from 'react-native';
|
|
5
|
+
|
|
6
|
+
export const AvatarMenu: FunctionComponent = () => {
|
|
7
|
+
return <></>;
|
|
8
|
+
// const [menuVisible, setMenuVisible] = useState(false);
|
|
9
|
+
// const { session, logout } = useSolidAuth();
|
|
10
|
+
// // TODO: Use WebId Resource to render a skeleton loader
|
|
11
|
+
// const webIdResource = useResource(session.webId);
|
|
12
|
+
// const profile = useSubject(SolidProfileShapeShapeType, session.webId);
|
|
13
|
+
// const renderAvatar = () => (
|
|
14
|
+
// <TouchableWithoutFeedback onPress={() => setMenuVisible(true)}>
|
|
15
|
+
// <Avatar
|
|
16
|
+
// source={{ uri: 'https://api.lorem.space/image/face?w=150&h=150' }}
|
|
17
|
+
// />
|
|
18
|
+
// </TouchableWithoutFeedback>
|
|
19
|
+
// );
|
|
20
|
+
// return (
|
|
21
|
+
// <Popover
|
|
22
|
+
// anchor={renderAvatar}
|
|
23
|
+
// visible={menuVisible}
|
|
24
|
+
// placement="bottom end"
|
|
25
|
+
// onBackdropPress={() => setMenuVisible(false)}
|
|
26
|
+
// style={styles.popover}
|
|
27
|
+
// >
|
|
28
|
+
// <Layout>
|
|
29
|
+
// <View style={styles.profileHeader}>
|
|
30
|
+
// <Avatar
|
|
31
|
+
// size="giant"
|
|
32
|
+
// source={{ uri: 'https://api.lorem.space/image/face?w=150&h=150' }}
|
|
33
|
+
// />
|
|
34
|
+
// <View style={styles.profileText}>
|
|
35
|
+
// <Text category="h6">{profile?.fn || ''}</Text>
|
|
36
|
+
// <Button size="tiny">Edit your profile</Button>
|
|
37
|
+
// </View>
|
|
38
|
+
// </View>
|
|
39
|
+
// <Divider />
|
|
40
|
+
// <ThemeToggleMenu />
|
|
41
|
+
// <Divider />
|
|
42
|
+
// <MenuItem
|
|
43
|
+
// onPress={logout}
|
|
44
|
+
// title="Log Out"
|
|
45
|
+
// accessoryLeft={(props) => <Icon {...props} name="log-out" />}
|
|
46
|
+
// />
|
|
47
|
+
// </Layout>
|
|
48
|
+
// </Popover>
|
|
49
|
+
// );
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const styles = StyleSheet.create({
|
|
53
|
+
popover: { width: 300, overflow: 'hidden', borderRadius: 12 },
|
|
54
|
+
profileHeader: { padding: 8, flexDirection: 'row', alignItems: 'center' },
|
|
55
|
+
profileText: {
|
|
56
|
+
marginLeft: 8,
|
|
57
|
+
justifyContent: 'space-around',
|
|
58
|
+
alignSelf: 'stretch',
|
|
59
|
+
alignItems: 'flex-start',
|
|
60
|
+
flex: 1,
|
|
61
|
+
},
|
|
62
|
+
});
|