init commit,
This commit is contained in:
102
99_references/voyager-main/src/core/App.tsx
Normal file
102
99_references/voyager-main/src/core/App.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { IonApp, setupIonicReact } from "@ionic/react";
|
||||
import { StoreProvider } from "../store";
|
||||
import {
|
||||
getAndroidNavMode,
|
||||
getDeviceMode,
|
||||
isInstalled,
|
||||
} from "../helpers/device";
|
||||
import TabbedRoutes from "../routes/TabbedRoutes";
|
||||
import Auth from "./Auth";
|
||||
import { AppContextProvider } from "../features/auth/AppContext";
|
||||
import Router from "../routes/common/Router";
|
||||
import BeforeInstallPromptProvider from "../features/pwa/BeforeInstallPromptProvider";
|
||||
import { UpdateContextProvider } from "../routes/pages/settings/update/UpdateContext";
|
||||
import GlobalStyles from "./GlobalStyles";
|
||||
import ConfigProvider from "../services/app";
|
||||
import { TabContextProvider } from "./TabContext";
|
||||
import { NavModes } from "capacitor-android-nav-mode";
|
||||
import { TextRecoveryStartupPrompt } from "../helpers/useTextRecovery";
|
||||
import HapticsListener from "./listeners/HapticsListener";
|
||||
import AndroidBackButton from "./listeners/AndroidBackButton";
|
||||
import { OptimizedRouterProvider } from "../helpers/useOptimizedIonRouter";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import AppCrash from "./AppCrash";
|
||||
|
||||
/* Core CSS required for Ionic components to work properly */
|
||||
import "@ionic/react/css/core.css";
|
||||
|
||||
/* Basic CSS for apps built with Ionic */
|
||||
import "@ionic/react/css/normalize.css";
|
||||
import "@ionic/react/css/structure.css";
|
||||
import "@ionic/react/css/typography.css";
|
||||
|
||||
/* Optional CSS utils that can be commented out */
|
||||
import "@ionic/react/css/padding.css";
|
||||
import "@ionic/react/css/float-elements.css";
|
||||
import "@ionic/react/css/text-alignment.css";
|
||||
import "@ionic/react/css/text-transformation.css";
|
||||
import "@ionic/react/css/flex-utils.css";
|
||||
import "@ionic/react/css/display.css";
|
||||
|
||||
import "@ionic/react/css/palettes/dark.class.css";
|
||||
|
||||
/* Setup global app lifecycle listeners */
|
||||
import "./listeners";
|
||||
import AppUrlListener from "./listeners/AppUrlListener";
|
||||
import { ResetStatusTap } from "./listeners/statusTap";
|
||||
|
||||
// index.tsx ensures android nav mode resolves before app is rendered
|
||||
(async () => {
|
||||
let navMode;
|
||||
try {
|
||||
navMode = await getAndroidNavMode();
|
||||
} catch (_) {
|
||||
// ignore errors
|
||||
}
|
||||
|
||||
setupIonicReact({
|
||||
mode: getDeviceMode(),
|
||||
statusTap: false, // custom implementation listeners/statusTap.ts
|
||||
swipeBackEnabled:
|
||||
isInstalled() &&
|
||||
getDeviceMode() === "ios" &&
|
||||
navMode !== NavModes.Gesture,
|
||||
});
|
||||
})();
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<ErrorBoundary FallbackComponent={AppCrash}>
|
||||
<ConfigProvider>
|
||||
<AppContextProvider>
|
||||
<StoreProvider>
|
||||
<GlobalStyles>
|
||||
<BeforeInstallPromptProvider>
|
||||
<UpdateContextProvider>
|
||||
<Router>
|
||||
<OptimizedRouterProvider>
|
||||
<AndroidBackButton />
|
||||
<ResetStatusTap />
|
||||
|
||||
<TabContextProvider>
|
||||
<IonApp>
|
||||
<HapticsListener />
|
||||
<AppUrlListener />
|
||||
<TextRecoveryStartupPrompt />
|
||||
|
||||
<Auth>
|
||||
<TabbedRoutes />
|
||||
</Auth>
|
||||
</IonApp>
|
||||
</TabContextProvider>
|
||||
</OptimizedRouterProvider>
|
||||
</Router>
|
||||
</UpdateContextProvider>
|
||||
</BeforeInstallPromptProvider>
|
||||
</GlobalStyles>
|
||||
</StoreProvider>
|
||||
</AppContextProvider>
|
||||
</ConfigProvider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
165
99_references/voyager-main/src/core/AppCrash.tsx
Normal file
165
99_references/voyager-main/src/core/AppCrash.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import { styled } from "@linaria/react";
|
||||
import { FallbackProps } from "react-error-boundary";
|
||||
import { isInstalled, isNative } from "../helpers/device";
|
||||
import { IonButton, IonIcon, IonLabel } from "@ionic/react";
|
||||
import { logoGithub } from "ionicons/icons";
|
||||
import { unloadServiceWorkerAndRefresh } from "../helpers/serviceWorker";
|
||||
import { memoryHistory } from "../routes/common/Router";
|
||||
import store from "../store";
|
||||
import { loggedInSelector } from "../features/auth/authSelectors";
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
||||
overflow: auto;
|
||||
|
||||
padding-top: max(env(safe-area-inset-top), 8px);
|
||||
padding-right: max(env(safe-area-inset-right), 8px);
|
||||
padding-bottom: max(env(safe-area-inset-bottom), 8px);
|
||||
padding-left: max(env(safe-area-inset-left), 8px);
|
||||
|
||||
color: var(--ion-text-color);
|
||||
`;
|
||||
|
||||
const Title = styled.h2``;
|
||||
|
||||
const Description = styled.div``;
|
||||
|
||||
export default function AppCrash({ error }: FallbackProps) {
|
||||
// Don't use useLocation/useAppSelector, because they are not available
|
||||
// (`<AppCrash />` is at the root of the document tree)
|
||||
const location = memoryHistory ? memoryHistory.location : window.location;
|
||||
const loggedIn = loggedInSelector(store.getState());
|
||||
|
||||
let crashData = `
|
||||
### Crash description
|
||||
|
||||
<!-- Write any information here to help us debug your crash! -->
|
||||
<!-- What were you doing when the crash occurred? -->
|
||||
|
||||
### Device and app metadata
|
||||
|
||||
- window.location.href: \`${window.location.href}\`
|
||||
- react-router location.pathname: \`${location.pathname}\`
|
||||
- Logged in? \`${loggedIn}\`
|
||||
- Native app? \`${isNative()}\`
|
||||
- Installed to home screen? \`${isInstalled()}\`
|
||||
- Voyager version: \`${APP_VERSION}\`
|
||||
- BUILD_FOSS_ONLY: \`${BUILD_FOSS_ONLY}\`
|
||||
- User agent: \`${navigator.userAgent}\`
|
||||
|
||||
### Crash data
|
||||
|
||||
Error: \`\`${error}\`\`
|
||||
|
||||
#### Stack trace
|
||||
|
||||
\`\`\`
|
||||
`.trim();
|
||||
|
||||
crashData = `${crashData}\n${error instanceof Error ? error.stack : "Not available"}`;
|
||||
|
||||
async function clearData() {
|
||||
if (
|
||||
!confirm(
|
||||
"Are you sure? This will log you out of all accounts and delete all local app data including app configuration, hidden posts and favorites.",
|
||||
)
|
||||
)
|
||||
return;
|
||||
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
|
||||
const dbs = await window.indexedDB.databases();
|
||||
for (const db of dbs) {
|
||||
if (db.name) window.indexedDB.deleteDatabase(db.name);
|
||||
}
|
||||
|
||||
alert("All data cleared.");
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Title>🫣 Gah! Voyager crashed!</Title>
|
||||
<Description>
|
||||
Voyager does not collect any data, so we would appreciate you
|
||||
voluntarily submitting this crash for us to investigate.
|
||||
</Description>
|
||||
<IonButton
|
||||
href={generateTruncatedCrashUrl(crashData)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
color="success"
|
||||
>
|
||||
<IonIcon icon={logoGithub} slot="start" />
|
||||
<IonLabel>Open Github issue with crash data</IonLabel>
|
||||
</IonButton>
|
||||
|
||||
<hr />
|
||||
|
||||
<Description>
|
||||
You can also try reloading the app to see if that solves the issue.
|
||||
{isNative() ? " Check the app store for an update, too." : ""}
|
||||
</Description>
|
||||
<IonButton onClick={unloadServiceWorkerAndRefresh}>Reload app</IonButton>
|
||||
|
||||
<hr />
|
||||
|
||||
<Description>
|
||||
If this crash is affecting many people, you can probably learn more{" "}
|
||||
<a
|
||||
href="https://lemmy.world/c/voyagerapp"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
at Voyager's Lemmy community
|
||||
</a>
|
||||
.
|
||||
</Description>
|
||||
|
||||
<Description>As a last resort, try clearing all app data.</Description>
|
||||
<IonButton color="danger" onClick={clearData}>
|
||||
Clear app data
|
||||
</IonButton>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
function generateCrashUrl(crashData: string): string {
|
||||
return `https://github.com/aeharding/voyager/issues/new?title=Crash&body=${encodeURIComponent(
|
||||
crashData,
|
||||
)}`;
|
||||
}
|
||||
|
||||
// The GitHub GET endpoint for opening a new issue
|
||||
// has a restriction for maximum length of a URL: 8192 bytes
|
||||
// https://github.com/cli/cli/pull/3271
|
||||
// https://github.com/cli/cli/issues/1575
|
||||
// https://github.com/cli/cli/blob/trunk/pkg/cmd/issue/create/create.go#L167
|
||||
// https://github.com/cli/cli/blob/trunk/utils/utils.go#L84
|
||||
const maxIssueBytes = 8150;
|
||||
|
||||
function getStrByteLength(str: string): number {
|
||||
return new TextEncoder().encode(str).length;
|
||||
}
|
||||
|
||||
function generateTruncatedCrashUrl(crashData: string): string {
|
||||
let url: string;
|
||||
let strLength = 1;
|
||||
|
||||
do {
|
||||
url = generateCrashUrl(crashData.slice(0, strLength));
|
||||
if (strLength === crashData.length) return url;
|
||||
strLength++;
|
||||
} while (getStrByteLength(url) < maxIssueBytes);
|
||||
|
||||
return url;
|
||||
}
|
114
99_references/voyager-main/src/core/Auth.tsx
Normal file
114
99_references/voyager-main/src/core/Auth.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import { useAppDispatch, useAppSelector } from "../store";
|
||||
import { updateConnectedInstance } from "../features/auth/authSlice";
|
||||
import { useLocation } from "react-router";
|
||||
import { getInboxCounts, syncMessages } from "../features/inbox/inboxSlice";
|
||||
import { useInterval } from "usehooks-ts";
|
||||
import usePageVisibility from "../helpers/usePageVisibility";
|
||||
import { getDefaultServer } from "../services/app";
|
||||
import BackgroundReportSync from "../features/moderation/BackgroundReportSync";
|
||||
import { getSiteIfNeeded, isAdminSelector } from "../features/auth/siteSlice";
|
||||
import { instanceSelector, jwtSelector } from "../features/auth/authSelectors";
|
||||
|
||||
interface AuthProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function Auth({ children }: AuthProps) {
|
||||
const dispatch = useAppDispatch();
|
||||
const jwt = useAppSelector(jwtSelector);
|
||||
const connectedInstance = useAppSelector(
|
||||
(state) => state.auth.connectedInstance,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getSiteIfNeeded());
|
||||
}, [dispatch, jwt, connectedInstance]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AuthLocation />
|
||||
{connectedInstance ? children : undefined}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Separate component so that it doesn't rerender react component tree on location change
|
||||
*/
|
||||
function AuthLocation() {
|
||||
const location = useLocation();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const pageVisibility = usePageVisibility();
|
||||
const jwt = useAppSelector(jwtSelector);
|
||||
|
||||
const selectedInstance = useAppSelector(instanceSelector);
|
||||
const connectedInstance = useAppSelector(
|
||||
(state) => state.auth.connectedInstance,
|
||||
);
|
||||
|
||||
const hasModdedSubs = useAppSelector(
|
||||
(state) =>
|
||||
!!state.site.response?.my_user?.moderates.length ||
|
||||
!!isAdminSelector(state),
|
||||
);
|
||||
|
||||
const shouldSyncMessages = useCallback(() => {
|
||||
return jwt && location.pathname.startsWith("/inbox/messages");
|
||||
}, [jwt, location]);
|
||||
|
||||
useEffect(() => {
|
||||
if (connectedInstance) return;
|
||||
|
||||
const potentialConnectedInstance = location.pathname.split("/")[2];
|
||||
|
||||
if (
|
||||
potentialConnectedInstance &&
|
||||
connectedInstance === potentialConnectedInstance
|
||||
)
|
||||
return;
|
||||
|
||||
if (selectedInstance) {
|
||||
dispatch(updateConnectedInstance(selectedInstance));
|
||||
} else if (potentialConnectedInstance?.includes(".")) {
|
||||
dispatch(updateConnectedInstance(potentialConnectedInstance));
|
||||
} else {
|
||||
dispatch(updateConnectedInstance(getDefaultServer()));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [location.pathname]);
|
||||
|
||||
useInterval(
|
||||
() => {
|
||||
if (!pageVisibility) return;
|
||||
if (!shouldSyncMessages()) return;
|
||||
|
||||
dispatch(syncMessages());
|
||||
},
|
||||
shouldSyncMessages() ? 1_000 * 15 : null,
|
||||
);
|
||||
|
||||
useInterval(() => {
|
||||
if (!pageVisibility) return;
|
||||
if (!jwt) return;
|
||||
|
||||
dispatch(getInboxCounts());
|
||||
}, 1_000 * 60);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pageVisibility) return;
|
||||
|
||||
dispatch(getInboxCounts());
|
||||
}, [pageVisibility, jwt, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pageVisibility) return;
|
||||
if (!shouldSyncMessages()) return;
|
||||
|
||||
dispatch(syncMessages());
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pageVisibility]);
|
||||
|
||||
return <>{hasModdedSubs && <BackgroundReportSync />}</>;
|
||||
}
|
147
99_references/voyager-main/src/core/GlobalStyles.tsx
Normal file
147
99_references/voyager-main/src/core/GlobalStyles.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import { useAppSelector } from "../store";
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
} from "react";
|
||||
import { StatusBar, Style } from "@capacitor/status-bar";
|
||||
import { isNative } from "../helpers/device";
|
||||
import { Keyboard, KeyboardStyle } from "@capacitor/keyboard";
|
||||
import useSystemDarkMode, {
|
||||
DARK_MEDIA_SELECTOR,
|
||||
} from "../helpers/useSystemDarkMode";
|
||||
import { css } from "@linaria/core";
|
||||
import { getThemeByStyle } from "./theme/AppThemes";
|
||||
|
||||
import "./theme/variables";
|
||||
import { AppThemeType } from "../services/db";
|
||||
import { stateWithLocalstorageItems as initialCriticalSettingsState } from "../features/settings/settingsSlice";
|
||||
|
||||
export const DARK_CLASSNAME = "ion-palette-dark";
|
||||
export const PURE_BLACK_CLASSNAME = "theme-pure-black";
|
||||
export const THEME_HAS_CUSTOM_BACKGROUND = "theme-has-custom-background";
|
||||
|
||||
function updateDocumentTheme(
|
||||
isDark: boolean,
|
||||
isPureBlack: boolean,
|
||||
theme: AppThemeType,
|
||||
) {
|
||||
const { primary, background, insetItemBackground, tabBarBackground } =
|
||||
getThemeByStyle(theme, isDark ? "dark" : "light");
|
||||
|
||||
document.documentElement.style.setProperty("--app-primary", primary);
|
||||
document.documentElement.style.setProperty(
|
||||
"--app-background",
|
||||
background ?? "",
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
"--app-inset-item-background",
|
||||
insetItemBackground ?? "",
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
"--app-tab-bar-background",
|
||||
tabBarBackground ?? "",
|
||||
);
|
||||
|
||||
const documentClasses = document.documentElement.classList;
|
||||
|
||||
if (background) {
|
||||
documentClasses.add(THEME_HAS_CUSTOM_BACKGROUND);
|
||||
} else {
|
||||
documentClasses.remove(THEME_HAS_CUSTOM_BACKGROUND);
|
||||
}
|
||||
|
||||
if (isDark && isPureBlack) {
|
||||
documentClasses.add(PURE_BLACK_CLASSNAME);
|
||||
} else {
|
||||
documentClasses.remove(PURE_BLACK_CLASSNAME);
|
||||
}
|
||||
|
||||
if (isDark) {
|
||||
documentClasses.add(DARK_CLASSNAME);
|
||||
} else {
|
||||
documentClasses.remove(DARK_CLASSNAME);
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent flash of white content and repaint before react component setup
|
||||
updateDocumentTheme(
|
||||
initialCriticalSettingsState.appearance.dark.usingSystemDarkMode
|
||||
? window.matchMedia(DARK_MEDIA_SELECTOR).matches
|
||||
: initialCriticalSettingsState.appearance.dark.userDarkMode,
|
||||
initialCriticalSettingsState.appearance.dark.pureBlack,
|
||||
initialCriticalSettingsState.appearance.theme,
|
||||
);
|
||||
|
||||
const fixedDeviceFontCss = css`
|
||||
--ion-dynamic-font: initial;
|
||||
`;
|
||||
|
||||
interface GlobalStylesProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function GlobalStyles({ children }: GlobalStylesProps) {
|
||||
const isDark = useComputeIsDark();
|
||||
const { fontSizeMultiplier, useSystemFontSize } = useAppSelector(
|
||||
(state) => state.settings.appearance.font,
|
||||
);
|
||||
|
||||
const { usingSystemDarkMode, pureBlack } = useAppSelector(
|
||||
(state) => state.settings.appearance.dark,
|
||||
);
|
||||
const theme = useAppSelector((state) => state.settings.appearance.theme);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (isNative()) {
|
||||
StatusBar.setStyle({ style: isDark ? Style.Dark : Style.Light });
|
||||
}
|
||||
}, [isDark]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (useSystemFontSize) {
|
||||
document.documentElement.classList.remove(fixedDeviceFontCss);
|
||||
document.documentElement.style.fontSize = "";
|
||||
} else {
|
||||
document.documentElement.classList.add(fixedDeviceFontCss);
|
||||
document.documentElement.style.fontSize = `${fontSizeMultiplier}rem`;
|
||||
}
|
||||
}, [useSystemFontSize, fontSizeMultiplier]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
updateDocumentTheme(isDark, pureBlack, theme);
|
||||
}, [theme, pureBlack, isDark]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNative()) return;
|
||||
|
||||
const keyboardStyle = (() => {
|
||||
if (usingSystemDarkMode) return KeyboardStyle.Default;
|
||||
|
||||
if (isDark) return KeyboardStyle.Dark;
|
||||
return KeyboardStyle.Light;
|
||||
})();
|
||||
|
||||
Keyboard.setStyle({ style: keyboardStyle });
|
||||
}, [isDark, usingSystemDarkMode]);
|
||||
|
||||
return <DarkContext.Provider value={isDark}>{children}</DarkContext.Provider>;
|
||||
}
|
||||
|
||||
function useComputeIsDark(): boolean {
|
||||
const systemDarkMode = useSystemDarkMode(); // sets up document listeners
|
||||
|
||||
const { userDarkMode, usingSystemDarkMode } = useAppSelector(
|
||||
(state) => state.settings.appearance.dark,
|
||||
);
|
||||
|
||||
return usingSystemDarkMode ? systemDarkMode : userDarkMode;
|
||||
}
|
||||
|
||||
// Cached
|
||||
export function useIsDark() {
|
||||
return useContext(DarkContext);
|
||||
}
|
||||
|
||||
const DarkContext = createContext(false);
|
59
99_references/voyager-main/src/core/TabContext.tsx
Normal file
59
99_references/voyager-main/src/core/TabContext.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import React, {
|
||||
MutableRefObject,
|
||||
createContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from "react";
|
||||
import { useLocation } from "react-router";
|
||||
|
||||
interface ITabContext {
|
||||
tabRef: MutableRefObject<string> | undefined;
|
||||
}
|
||||
|
||||
export const TabContext = createContext<ITabContext>({
|
||||
tabRef: undefined,
|
||||
});
|
||||
|
||||
/**
|
||||
* The reason for this, instead of useLocation() in components directly to get tab name,
|
||||
* is that it does not trigger a rerender on navigation changes.
|
||||
*/
|
||||
export function TabContextProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const location = useLocation();
|
||||
|
||||
const tab = location.pathname.split("/")[1]!;
|
||||
|
||||
const memoized = useMemo(
|
||||
() => (
|
||||
<TabContextProviderInternals tab={tab}>
|
||||
{children}
|
||||
</TabContextProviderInternals>
|
||||
),
|
||||
[tab, children],
|
||||
);
|
||||
|
||||
return memoized;
|
||||
}
|
||||
|
||||
function TabContextProviderInternals({
|
||||
tab,
|
||||
children,
|
||||
}: {
|
||||
tab: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const tabRef = useRef(tab);
|
||||
|
||||
useEffect(() => {
|
||||
tabRef.current = tab;
|
||||
}, [tab]);
|
||||
|
||||
const value = useMemo(() => ({ tabRef }), []);
|
||||
|
||||
return <TabContext.Provider value={value}>{children}</TabContext.Provider>;
|
||||
}
|
325
99_references/voyager-main/src/core/globalCssOverrides.ts
Normal file
325
99_references/voyager-main/src/core/globalCssOverrides.ts
Normal file
@@ -0,0 +1,325 @@
|
||||
import { css } from "@linaria/core";
|
||||
|
||||
import "./syntaxHighlightCss";
|
||||
|
||||
export default css`
|
||||
:global() {
|
||||
:root {
|
||||
--sat: env(safe-area-inset-top);
|
||||
--sar: env(safe-area-inset-right);
|
||||
--sab: env(safe-area-inset-bottom);
|
||||
--sal: env(safe-area-inset-left);
|
||||
}
|
||||
|
||||
html {
|
||||
-webkit-text-size-adjust: 100%; /* Prevent font scaling in landscape while allowing user zoom */
|
||||
}
|
||||
|
||||
.ReactCollapse--collapse {
|
||||
transition: height 200ms;
|
||||
}
|
||||
|
||||
ion-tab-button {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
ion-router-outlet > .ion-page > ion-header {
|
||||
// Header doesn't support dynamic font size
|
||||
ion-button,
|
||||
ion-back-button,
|
||||
ion-title {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
// Ionic's default font size for icons in header is a bit too large
|
||||
ion-button.in-toolbar ion-icon {
|
||||
font-size: 1.35em !important;
|
||||
}
|
||||
|
||||
ion-back-button.md::part(text) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
ion-router-outlet ion-list.list-inset ion-item {
|
||||
--background: var(
|
||||
--ion-tab-bar-background,
|
||||
var(--ion-background-color-step-50, #fff)
|
||||
);
|
||||
}
|
||||
|
||||
.left-align-buttons
|
||||
.action-sheet-button:not(.action-sheet-cancel)
|
||||
.action-sheet-button-inner.sc-ion-action-sheet-ios:not(
|
||||
.action-sheet-group-cancel
|
||||
) {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.mod .action-sheet-button:not(.action-sheet-destructive),
|
||||
.mod .action-sheet-button:hover:not(.action-sheet-destructive),
|
||||
.mod.action-sheet-button:not(.action-sheet-destructive),
|
||||
.mod.action-sheet-button:hover:not(.action-sheet-destructive) {
|
||||
--color: var(--ion-color-success-shade);
|
||||
color: var(--color);
|
||||
}
|
||||
|
||||
.mod.alert-button {
|
||||
color: var(--ion-color-success-shade);
|
||||
}
|
||||
|
||||
.admin-local .action-sheet-button,
|
||||
.admin-local .action-sheet-button:hover,
|
||||
.admin-local.action-sheet-button,
|
||||
.admin-local.action-sheet-button:hover,
|
||||
.admin-remote .action-sheet-button,
|
||||
.admin-remote .action-sheet-button:hover,
|
||||
.admin-remote.action-sheet-button,
|
||||
.admin-remote.action-sheet-button:hover {
|
||||
--color: var(--ion-color-danger-shade);
|
||||
color: var(--color);
|
||||
}
|
||||
|
||||
.report-reasons .action-sheet-title {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
/* https://github.com/ionic-team/ionic-framework/issues/27777#issuecomment-1631522283 */
|
||||
.action-sheet-height-fix {
|
||||
--height: 100%;
|
||||
}
|
||||
|
||||
ion-modal.small {
|
||||
--height: 50%;
|
||||
--width: 85%;
|
||||
--max-width: 400px;
|
||||
--max-height: 500px;
|
||||
--border-radius: 16px;
|
||||
--box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1),
|
||||
0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
ion-modal.small ion-header ion-toolbar:first-of-type {
|
||||
padding-top: 0px;
|
||||
}
|
||||
|
||||
ion-modal.transparent-scroll {
|
||||
--background: none;
|
||||
--height: auto;
|
||||
--box-shadow: none;
|
||||
|
||||
&.show-modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
ion-modal.transparent-scroll .ion-page {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
ion-alert.preserve-newlines {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.pswp__img {
|
||||
-webkit-touch-callout: default;
|
||||
}
|
||||
|
||||
// gets in the way of non-post preview more actions button
|
||||
.pswp__preloader {
|
||||
display: none;
|
||||
}
|
||||
|
||||
ion-action-sheet .detail::before {
|
||||
content: "";
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
|
||||
/* Ion chevron right icon */
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><path fill="none" stroke="%2392949c44" stroke-linecap="round" stroke-linejoin="round" stroke-width="48" d="M184 112l144 144-144 144"/></svg>');
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
ion-action-sheet .action-sheet-selected {
|
||||
font-weight: normal !important;
|
||||
}
|
||||
|
||||
ion-action-sheet .action-sheet-selected.action-sheet-selected:after {
|
||||
background: none;
|
||||
}
|
||||
|
||||
/* Double class to override ionic specificity */
|
||||
ion-action-sheet .action-sheet-selected .action-sheet-button-inner::after {
|
||||
content: "";
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
|
||||
/* Ion check icon */
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><path fill="none" stroke="%233880ff" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M416 128L192 384l-96-96"/></svg>');
|
||||
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
ion-action-sheet
|
||||
.action-sheet-selected.detail
|
||||
.action-sheet-button-inner::after {
|
||||
right: 40px;
|
||||
}
|
||||
|
||||
ion-action-sheet ion-icon {
|
||||
flex-shrink: 0;
|
||||
margin-inline-end: 16px !important;
|
||||
}
|
||||
|
||||
.ios .action-sheet-button:not(.action-sheet-cancel) {
|
||||
padding-inline-end: 0 !important;
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
|
||||
.ios .left-align-buttons .action-sheet-button:not(.action-sheet-cancel) {
|
||||
padding-inline-start: 14px;
|
||||
}
|
||||
|
||||
.action-sheet-button-inner {
|
||||
mask-image: linear-gradient(
|
||||
90deg,
|
||||
rgba(0, 0, 0, 1) 0%,
|
||||
rgba(0, 0, 0, 1) calc(100% - 16px),
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
/* photoswipe */
|
||||
|
||||
.pswp__top-bar {
|
||||
top: env(safe-area-inset-top, 0);
|
||||
}
|
||||
|
||||
/* Let IonActionSheet be on top */
|
||||
.pswp {
|
||||
--pswp-root-z-index: 100;
|
||||
}
|
||||
|
||||
.ion-content-scroll-host {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.virtual-scroller {
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
.ios {
|
||||
.ion-content-scroll-host::before,
|
||||
.ion-content-scroll-host::after {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.ion-content-scroll-host::before {
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.ion-content-scroll-host::after {
|
||||
bottom: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
ion-fab-button {
|
||||
--background-focused: var(--ion-color-primary);
|
||||
--background-activated: var(--ion-color-primary);
|
||||
--background-hover: var(--ion-color-primary);
|
||||
}
|
||||
|
||||
ion-fab.fab-vertical-top {
|
||||
top: 15vh;
|
||||
}
|
||||
|
||||
/**
|
||||
* This patch reverts the following Ionic change:
|
||||
* https://github.com/ionic-team/ionic-framework/pull/28246/files#diff-8371750710eecf6a609337a240b7db295073f746f1cda13248f9780cc6f6ff24L159
|
||||
*
|
||||
* Private discord discussion: https://discord.com/channels/520266681499779082/1128001453676568637/1162812415054983198
|
||||
*
|
||||
* Fixes #780
|
||||
*/
|
||||
.ion-page {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
ion-toast {
|
||||
font-weight: 600;
|
||||
--height: 45px;
|
||||
}
|
||||
|
||||
/* Center toast message and icon */
|
||||
ion-toast.center::part(container) {
|
||||
display: inline-flex;
|
||||
transform: translateX(-50%);
|
||||
left: 50%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Make header buttons closer together (room for mod button) */
|
||||
.sc-ion-buttons-ios-s ion-button {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Add minimum padding for top dismiss area of action sheet */
|
||||
.action-sheet-group.sc-ion-action-sheet-ios:first-child {
|
||||
margin-top: min(15vh, 70px);
|
||||
}
|
||||
|
||||
#share-as-image-root {
|
||||
font-size: 16px;
|
||||
|
||||
--width: 330px;
|
||||
color: var(--ion-text-color);
|
||||
|
||||
position: absolute;
|
||||
width: var(--width);
|
||||
right: calc(var(--width) * -1);
|
||||
|
||||
a {
|
||||
/* Hack for long links breaking */
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
#share-as-image-root ion-item {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
ion-modal.save-as-image-modal {
|
||||
--height: auto;
|
||||
--max-width: 470px;
|
||||
}
|
||||
|
||||
#share-as-image-root video {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.collapse-md-margins {
|
||||
> *:first-child,
|
||||
> *:first-child > *:first-child,
|
||||
> *:first-child > *:first-child > *:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
> *:last-child,
|
||||
> *:last-child > *:last-child,
|
||||
> *:last-child > *:last-child > *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
@@ -0,0 +1,35 @@
|
||||
import { App } from "@capacitor/app";
|
||||
import { BackButtonEventDetail } from "@ionic/core";
|
||||
import { useEffect } from "react";
|
||||
import { useOptimizedIonRouter } from "../../helpers/useOptimizedIonRouter";
|
||||
|
||||
export default function AndroidBackButton() {
|
||||
const router = useOptimizedIonRouter();
|
||||
|
||||
// Back button handling for Android native app
|
||||
useEffect(() => {
|
||||
const backButtonHandler = (ev: CustomEvent<BackButtonEventDetail>) => {
|
||||
ev.detail.register(-1, () => {
|
||||
// pswp is the gallery component. It pushes state, but the router isn't aware.
|
||||
// So if that's open, don't close the app just yet
|
||||
if (!router.canGoBack() && !document.querySelector(".pswp--open")) {
|
||||
App.exitApp();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener(
|
||||
"ionBackButton",
|
||||
backButtonHandler as EventListener,
|
||||
);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener(
|
||||
"ionBackButton",
|
||||
backButtonHandler as EventListener,
|
||||
);
|
||||
};
|
||||
}, [router]);
|
||||
|
||||
return null;
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
import { App } from "@capacitor/app";
|
||||
import { useEffect, useRef } from "react";
|
||||
import useLemmyUrlHandler from "../../features/shared/useLemmyUrlHandler";
|
||||
import useEvent from "../../helpers/useEvent";
|
||||
import { useAppSelector } from "../../store";
|
||||
import useAppToast from "../../helpers/useAppToast";
|
||||
import { deepLinkFailed } from "../../helpers/toastMessages";
|
||||
import { normalizeObjectUrl } from "../../features/resolve/resolveSlice";
|
||||
|
||||
export default function AppUrlListener() {
|
||||
const { redirectToLemmyObjectIfNeeded } = useLemmyUrlHandler();
|
||||
const knownInstances = useAppSelector(
|
||||
(state) => state.instances.knownInstances,
|
||||
);
|
||||
const connectedInstance = useAppSelector(
|
||||
(state) => state.auth.connectedInstance,
|
||||
);
|
||||
const deepLinkReady = useAppSelector((state) => state.deepLinkReady.ready);
|
||||
|
||||
const appUrlToOpen = useRef<string | undefined>();
|
||||
const presentToast = useAppToast();
|
||||
|
||||
const notReady =
|
||||
!knownInstances ||
|
||||
knownInstances === "pending" ||
|
||||
!connectedInstance ||
|
||||
!deepLinkReady;
|
||||
|
||||
const onAppUrl = useEvent(async (url: string) => {
|
||||
if (notReady) {
|
||||
appUrlToOpen.current = url;
|
||||
return;
|
||||
}
|
||||
|
||||
// wait for router to get into a good state before pushing
|
||||
// (needed for pushing user profiles from app startup)
|
||||
const resolved = await redirectToLemmyObjectIfNeeded(url);
|
||||
|
||||
if (!resolved) presentToast(deepLinkFailed);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
App.addListener("appUrlOpen", (event) => {
|
||||
onAppUrl(normalizeObjectUrl(event.url));
|
||||
});
|
||||
}, [onAppUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (notReady) return;
|
||||
if (!appUrlToOpen.current) return;
|
||||
|
||||
onAppUrl(appUrlToOpen.current);
|
||||
appUrlToOpen.current = undefined;
|
||||
}, [notReady, onAppUrl]);
|
||||
|
||||
return null;
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
import { useEffect } from "react";
|
||||
import useHapticFeedback from "../../helpers/useHapticFeedback";
|
||||
import { ImpactStyle } from "@capacitor/haptics";
|
||||
|
||||
export default function HapticsListener() {
|
||||
const vibrate = useHapticFeedback();
|
||||
|
||||
useEffect(() => {
|
||||
const handleToggleChange = (e: Event) => {
|
||||
if (!(e.target instanceof HTMLElement)) return;
|
||||
if (e.target.tagName !== "ION-TOGGLE") return;
|
||||
|
||||
vibrate({ style: ImpactStyle.Light });
|
||||
};
|
||||
|
||||
const handleActionSheetWillPresent = () => {
|
||||
vibrate({ style: ImpactStyle.Light });
|
||||
};
|
||||
|
||||
document.addEventListener("ionChange", handleToggleChange);
|
||||
document.addEventListener(
|
||||
"ionActionSheetWillPresent",
|
||||
handleActionSheetWillPresent,
|
||||
);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("ionChange", handleToggleChange);
|
||||
document.removeEventListener(
|
||||
"ionActionSheetWillPresent",
|
||||
handleActionSheetWillPresent,
|
||||
);
|
||||
};
|
||||
}, [vibrate]);
|
||||
|
||||
return null;
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
import { isAndroid, isNative } from "../../helpers/device";
|
||||
import { SafeArea, SafeAreaInsets } from "capacitor-plugin-safe-area";
|
||||
import { StatusBar } from "@capacitor/status-bar";
|
||||
import { Keyboard } from "@capacitor/keyboard";
|
||||
|
||||
// Android safe area inset management is bad, we have to do it manually
|
||||
if (isNative() && isAndroid()) {
|
||||
let keyboardShowing = false;
|
||||
|
||||
const updateInsets = ({ insets }: SafeAreaInsets) => {
|
||||
for (const [key, value] of Object.entries(insets)) {
|
||||
document.documentElement.style.setProperty(
|
||||
`--ion-safe-area-${key}`,
|
||||
// if keyboard open, assume no safe area inset
|
||||
`${keyboardShowing && key === "bottom" ? 0 : value}px`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
SafeArea.getSafeAreaInsets().then(updateInsets);
|
||||
SafeArea.addListener("safeAreaChanged", updateInsets);
|
||||
StatusBar.setOverlaysWebView({ overlay: true });
|
||||
|
||||
Keyboard.addListener("keyboardWillShow", () => {
|
||||
keyboardShowing = true;
|
||||
SafeArea.getSafeAreaInsets().then(updateInsets);
|
||||
});
|
||||
Keyboard.addListener("keyboardWillHide", () => {
|
||||
keyboardShowing = false;
|
||||
SafeArea.getSafeAreaInsets().then(updateInsets);
|
||||
});
|
||||
}
|
5
99_references/voyager-main/src/core/listeners/index.ts
Normal file
5
99_references/voyager-main/src/core/listeners/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import "./androidSafeArea";
|
||||
import "./keyboardPageResizer";
|
||||
import "./statusTap";
|
||||
import "./network/listener";
|
||||
import "./ionActivatable";
|
@@ -0,0 +1,15 @@
|
||||
import { stopIonicTapClick } from "../../helpers/ionic";
|
||||
|
||||
/**
|
||||
* This prevents the `ion-activatable` tap highlight
|
||||
* when tapping buttons and other things within the activatable ion-item
|
||||
**/
|
||||
function onPreventIonicTapClick(e: MouseEvent | TouchEvent) {
|
||||
if (!(e.target instanceof HTMLElement)) return;
|
||||
if (!e.target.closest("ion-button,a,img,input,button")) return;
|
||||
|
||||
stopIonicTapClick();
|
||||
}
|
||||
|
||||
document.addEventListener("touchstart", onPreventIonicTapClick);
|
||||
document.addEventListener("mousedown", onPreventIonicTapClick);
|
@@ -0,0 +1,28 @@
|
||||
import { isPlatform } from "@ionic/core";
|
||||
import { Keyboard } from "@capacitor/keyboard";
|
||||
import { isNative } from "../../helpers/device";
|
||||
|
||||
// Code from:
|
||||
// https://github.com/ionic-team/capacitor/issues/1540#issuecomment-735221275
|
||||
//
|
||||
// Once the following issue is fixed:
|
||||
// https://github.com/ionic-team/capacitor-plugins/issues/1904
|
||||
//
|
||||
// 1. Remove this code
|
||||
// 2. Change capacitor.config.ts keyboard resize = "none" to "ionic"
|
||||
|
||||
if (isNative() && isPlatform("ios")) {
|
||||
Keyboard.addListener("keyboardWillShow", (e) => {
|
||||
const app = document.querySelector("ion-app");
|
||||
if (!(app instanceof HTMLElement)) return;
|
||||
|
||||
app.style.marginBottom = `${e.keyboardHeight}px`;
|
||||
});
|
||||
|
||||
Keyboard.addListener("keyboardWillHide", () => {
|
||||
const app = document.querySelector("ion-app");
|
||||
if (!(app instanceof HTMLElement)) return;
|
||||
|
||||
app.style.marginBottom = "0px";
|
||||
});
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
import store from "../../../store";
|
||||
import { getConnectionType, updateConnectionType } from "./networkSlice";
|
||||
import { Network } from "@capacitor/network";
|
||||
import { isNative } from "../../../helpers/device";
|
||||
|
||||
(async () => {
|
||||
if (!isNative()) return;
|
||||
|
||||
await store.dispatch(getConnectionType());
|
||||
|
||||
Network.addListener("networkStatusChange", ({ connectionType }) => {
|
||||
store.dispatch(updateConnectionType(connectionType));
|
||||
});
|
||||
})();
|
@@ -0,0 +1,38 @@
|
||||
import { PayloadAction, createAsyncThunk, createSlice } from "@reduxjs/toolkit";
|
||||
import { ConnectionType, Network } from "@capacitor/network";
|
||||
|
||||
interface NetworkState {
|
||||
connectionType: ConnectionType;
|
||||
}
|
||||
|
||||
const initialState: NetworkState = {
|
||||
connectionType: "unknown",
|
||||
};
|
||||
|
||||
export const networkSlice = createSlice({
|
||||
name: "network",
|
||||
initialState,
|
||||
reducers: {
|
||||
updateConnectionType(state, action: PayloadAction<ConnectionType>) {
|
||||
state.connectionType = action.payload;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(getConnectionType.fulfilled, (state, action) => {
|
||||
state.connectionType = action.payload;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { updateConnectionType } = networkSlice.actions;
|
||||
|
||||
export default networkSlice.reducer;
|
||||
|
||||
export const getConnectionType = createAsyncThunk(
|
||||
"network/getConnectionType",
|
||||
async () => {
|
||||
const { connectionType } = await Network.getStatus();
|
||||
|
||||
return connectionType;
|
||||
},
|
||||
);
|
@@ -0,0 +1,20 @@
|
||||
import { OAutoplayMediaType } from "../../../services/db";
|
||||
import { useAppSelector } from "../../../store";
|
||||
|
||||
export default function useShouldAutoplay() {
|
||||
const autoplayMedia = useAppSelector(
|
||||
(state) => state.settings.general.posts.autoplayMedia,
|
||||
);
|
||||
const connectionType = useAppSelector(
|
||||
(state) => state.network.connectionType,
|
||||
);
|
||||
|
||||
switch (autoplayMedia) {
|
||||
case OAutoplayMediaType.Always:
|
||||
return true;
|
||||
case OAutoplayMediaType.Never:
|
||||
return false;
|
||||
case OAutoplayMediaType.WifiOnly:
|
||||
return connectionType === "wifi";
|
||||
}
|
||||
}
|
63
99_references/voyager-main/src/core/listeners/statusTap.ts
Normal file
63
99_references/voyager-main/src/core/listeners/statusTap.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { useEffect } from "react";
|
||||
import { findCurrentPage } from "../../helpers/ionic";
|
||||
import { Browser } from "@capacitor/browser";
|
||||
import { useLocation } from "react-router";
|
||||
|
||||
let savedScrollTop = 0;
|
||||
|
||||
/**
|
||||
* statusTap is emitted with open full screen browser
|
||||
*/
|
||||
let browserOpen = false;
|
||||
|
||||
export function notifyStatusTapThatBrowserWasOpened() {
|
||||
browserOpen = true;
|
||||
}
|
||||
|
||||
Browser.addListener("browserFinished", () => {
|
||||
browserOpen = false;
|
||||
});
|
||||
|
||||
// statusTap is a capacitor (native app), iOS-only event
|
||||
// https://capacitorjs.com/docs/apis/status-bar#example
|
||||
//
|
||||
// This custom implementation allows scrolling back down in
|
||||
// the feed by tapping again after initially tapping to
|
||||
// scroll to top
|
||||
window.addEventListener("statusTap", () => {
|
||||
if (browserOpen) return;
|
||||
|
||||
const page = findCurrentPage();
|
||||
|
||||
if (!page) return;
|
||||
|
||||
// TODO this logic is semi-duplicated in scrollUpIfNeeded.ts
|
||||
// and should probably be abstracted
|
||||
const scroll =
|
||||
page.querySelector(".virtual-scroller") ??
|
||||
page.shadowRoot?.querySelector(".inner-scroll");
|
||||
|
||||
if (!scroll) return;
|
||||
|
||||
if (scroll.scrollTop) {
|
||||
savedScrollTop = scroll.scrollTop;
|
||||
|
||||
scroll.scrollTo({ top: 0, behavior: "smooth" });
|
||||
} else {
|
||||
scroll.scrollTo({ top: savedScrollTop, behavior: "smooth" });
|
||||
}
|
||||
});
|
||||
|
||||
export function resetSavedStatusTap() {
|
||||
savedScrollTop = 0;
|
||||
}
|
||||
|
||||
export function ResetStatusTap() {
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
resetSavedStatusTap();
|
||||
}, [location]);
|
||||
|
||||
return null;
|
||||
}
|
170
99_references/voyager-main/src/core/syntaxHighlightCss.ts
Normal file
170
99_references/voyager-main/src/core/syntaxHighlightCss.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import { css } from "@linaria/core";
|
||||
|
||||
export default css`
|
||||
:global() {
|
||||
html.ion-palette-dark {
|
||||
.hljs-doctag,
|
||||
.hljs-keyword,
|
||||
.hljs-meta .hljs-keyword,
|
||||
.hljs-template-tag,
|
||||
.hljs-template-variable,
|
||||
.hljs-type,
|
||||
.hljs-variable.language_ {
|
||||
color: #ff7b72;
|
||||
}
|
||||
.hljs-title,
|
||||
.hljs-title.class_,
|
||||
.hljs-title.class_.inherited__,
|
||||
.hljs-title.function_ {
|
||||
color: #d2a8ff;
|
||||
}
|
||||
.hljs-attr,
|
||||
.hljs-attribute,
|
||||
.hljs-literal,
|
||||
.hljs-meta,
|
||||
.hljs-number,
|
||||
.hljs-operator,
|
||||
.hljs-variable,
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-class,
|
||||
.hljs-selector-id {
|
||||
color: #79c0ff;
|
||||
}
|
||||
.hljs-regexp,
|
||||
.hljs-string,
|
||||
.hljs-meta .hljs-string {
|
||||
color: #a5d6ff;
|
||||
}
|
||||
.hljs-built_in,
|
||||
.hljs-symbol {
|
||||
color: #ffa657;
|
||||
}
|
||||
.hljs-comment,
|
||||
.hljs-code,
|
||||
.hljs-formula {
|
||||
color: #8b949e;
|
||||
}
|
||||
.hljs-name,
|
||||
.hljs-quote,
|
||||
.hljs-selector-tag,
|
||||
.hljs-selector-pseudo {
|
||||
color: #7ee787;
|
||||
}
|
||||
.hljs-subst {
|
||||
color: #c9d1d9;
|
||||
}
|
||||
.hljs-section {
|
||||
color: #1f6feb;
|
||||
font-weight: bold;
|
||||
}
|
||||
.hljs-bullet {
|
||||
color: #f2cc60;
|
||||
}
|
||||
.hljs-emphasis {
|
||||
color: #c9d1d9;
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-strong {
|
||||
color: #c9d1d9;
|
||||
font-weight: bold;
|
||||
}
|
||||
.hljs-addition {
|
||||
color: #aff5b4;
|
||||
background-color: #033a16;
|
||||
}
|
||||
.hljs-deletion {
|
||||
color: #ffdcd7;
|
||||
background-color: #67060c;
|
||||
}
|
||||
}
|
||||
|
||||
html:not(.ion-palette-dark) {
|
||||
.hljs-doctag,
|
||||
.hljs-keyword,
|
||||
.hljs-meta .hljs-keyword,
|
||||
.hljs-template-tag,
|
||||
.hljs-template-variable,
|
||||
.hljs-type,
|
||||
.hljs-variable.language_ {
|
||||
color: #d73a49;
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-title.class_,
|
||||
.hljs-title.class_.inherited__,
|
||||
.hljs-title.function_ {
|
||||
color: #6f42c1;
|
||||
}
|
||||
|
||||
.hljs-attr,
|
||||
.hljs-attribute,
|
||||
.hljs-literal,
|
||||
.hljs-meta,
|
||||
.hljs-number,
|
||||
.hljs-operator,
|
||||
.hljs-variable,
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-class,
|
||||
.hljs-selector-id {
|
||||
color: #005cc5;
|
||||
}
|
||||
|
||||
.hljs-regexp,
|
||||
.hljs-string,
|
||||
.hljs-meta .hljs-string {
|
||||
color: #032f62;
|
||||
}
|
||||
|
||||
.hljs-built_in,
|
||||
.hljs-symbol {
|
||||
color: #e36209;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-code,
|
||||
.hljs-formula {
|
||||
color: #6a737d;
|
||||
}
|
||||
|
||||
.hljs-name,
|
||||
.hljs-quote,
|
||||
.hljs-selector-tag,
|
||||
.hljs-selector-pseudo {
|
||||
color: #22863a;
|
||||
}
|
||||
|
||||
.hljs-subst {
|
||||
color: #24292e;
|
||||
}
|
||||
|
||||
.hljs-section {
|
||||
color: #005cc5;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-bullet {
|
||||
color: #735c0f;
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
color: #24292e;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
color: #24292e;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-addition {
|
||||
color: #22863a;
|
||||
background-color: #f0fff4;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
color: #b31d28;
|
||||
background-color: #ffeef0;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
130
99_references/voyager-main/src/core/theme/AppThemes.ts
Normal file
130
99_references/voyager-main/src/core/theme/AppThemes.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { AppThemeType } from "../../services/db";
|
||||
|
||||
interface Theme {
|
||||
light: Colors;
|
||||
dark: Colors;
|
||||
}
|
||||
|
||||
interface Colors {
|
||||
primary: string;
|
||||
background?: string;
|
||||
insetItemBackground?: string;
|
||||
tabBarBackground?: string;
|
||||
}
|
||||
|
||||
export function getTheme(appTheme: AppThemeType): Theme {
|
||||
switch (appTheme) {
|
||||
case "default":
|
||||
return {
|
||||
light: {
|
||||
primary: "#3880ff",
|
||||
},
|
||||
dark: {
|
||||
primary: "#428cff",
|
||||
},
|
||||
};
|
||||
case "mario":
|
||||
return {
|
||||
light: {
|
||||
primary: "color(display-p3 1 0 0)",
|
||||
},
|
||||
dark: {
|
||||
primary: "#db1f1f",
|
||||
},
|
||||
};
|
||||
case "pistachio":
|
||||
return {
|
||||
light: {
|
||||
primary: "color(display-p3 0 0.7 0)",
|
||||
},
|
||||
dark: {
|
||||
primary: "color(display-p3 0 0.6 0)",
|
||||
},
|
||||
};
|
||||
case "pumpkin":
|
||||
return {
|
||||
light: {
|
||||
primary: "color(display-p3 1 0.5 0)",
|
||||
},
|
||||
dark: {
|
||||
primary: "#DF6F0E",
|
||||
},
|
||||
};
|
||||
case "uv":
|
||||
return {
|
||||
light: {
|
||||
primary: "color(display-p3 0.5 0 1)",
|
||||
},
|
||||
dark: {
|
||||
primary: "#942AD4",
|
||||
},
|
||||
};
|
||||
case "mint":
|
||||
return {
|
||||
light: {
|
||||
primary: "#36BB97",
|
||||
},
|
||||
dark: {
|
||||
primary: "#53C391",
|
||||
},
|
||||
};
|
||||
case "dracula":
|
||||
return {
|
||||
light: {
|
||||
primary: "#AD81FF",
|
||||
},
|
||||
dark: {
|
||||
primary: "#AD81FF",
|
||||
background: "#1A1D29",
|
||||
insetItemBackground: "#12141C",
|
||||
tabBarBackground: "#12141C",
|
||||
},
|
||||
};
|
||||
case "tangerine":
|
||||
return {
|
||||
light: {
|
||||
primary: "#FF4500",
|
||||
},
|
||||
dark: {
|
||||
primary: "#FF4500",
|
||||
},
|
||||
};
|
||||
case "sunset":
|
||||
return {
|
||||
light: {
|
||||
primary: "#FE6C09",
|
||||
background: "#FFE2D0",
|
||||
insetItemBackground: "#F1D8C7",
|
||||
tabBarBackground: "#F1D8C7",
|
||||
},
|
||||
dark: {
|
||||
primary: "#FE7C00",
|
||||
background: "#000E29",
|
||||
insetItemBackground: "#11213C",
|
||||
tabBarBackground: "#000A1F",
|
||||
},
|
||||
};
|
||||
case "outrun":
|
||||
return {
|
||||
light: {
|
||||
primary: "#C400A5",
|
||||
background: "#BAC1D1",
|
||||
insetItemBackground: "#CFD7E8",
|
||||
tabBarBackground: "#C1C8D9",
|
||||
},
|
||||
dark: {
|
||||
primary: "#F335C5",
|
||||
background: "#081D47",
|
||||
insetItemBackground: "#061636",
|
||||
tabBarBackground: "#041129",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function getThemeByStyle(
|
||||
appTheme: AppThemeType,
|
||||
style: "light" | "dark",
|
||||
): Colors {
|
||||
return getTheme(appTheme)[style];
|
||||
}
|
378
99_references/voyager-main/src/core/theme/variables.ts
Normal file
378
99_references/voyager-main/src/core/theme/variables.ts
Normal file
@@ -0,0 +1,378 @@
|
||||
import { css } from "@linaria/core";
|
||||
|
||||
export const baseVariables = css`
|
||||
:global() {
|
||||
:root {
|
||||
--ion-text-color: #000;
|
||||
|
||||
/** primary **/
|
||||
--ion-color-primary: #3880ff;
|
||||
--ion-color-primary-rgb: 56, 128, 255;
|
||||
--ion-color-primary-contrast: #ffffff;
|
||||
--ion-color-primary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-primary-shade: #3171e0;
|
||||
--ion-color-primary-tint: #4c8dff;
|
||||
|
||||
/** secondary **/
|
||||
--ion-color-secondary: #3dc2ff;
|
||||
--ion-color-secondary-rgb: 61, 194, 255;
|
||||
--ion-color-secondary-contrast: #ffffff;
|
||||
--ion-color-secondary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-secondary-shade: #36abe0;
|
||||
--ion-color-secondary-tint: #50c8ff;
|
||||
|
||||
/** tertiary **/
|
||||
--ion-color-tertiary: #5260ff;
|
||||
--ion-color-tertiary-rgb: 82, 96, 255;
|
||||
--ion-color-tertiary-contrast: #ffffff;
|
||||
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-tertiary-shade: #4854e0;
|
||||
--ion-color-tertiary-tint: #6370ff;
|
||||
|
||||
/** success **/
|
||||
--ion-color-success: #07be02;
|
||||
--ion-color-success-rgb: 7, 190, 2;
|
||||
--ion-color-success-contrast: #ffffff;
|
||||
--ion-color-success-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-success-shade: #06a702;
|
||||
--ion-color-success-tint: #20c51b;
|
||||
|
||||
/** warning **/
|
||||
--ion-color-warning: #ffc409;
|
||||
--ion-color-warning-rgb: 255, 196, 9;
|
||||
--ion-color-warning-contrast: #000000;
|
||||
--ion-color-warning-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-warning-shade: #e0ac08;
|
||||
--ion-color-warning-tint: #ffca22;
|
||||
|
||||
/** danger **/
|
||||
--ion-color-danger: #eb445a;
|
||||
--ion-color-danger-rgb: 235, 68, 90;
|
||||
--ion-color-danger-contrast: #ffffff;
|
||||
--ion-color-danger-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-danger-shade: #cf3c4f;
|
||||
--ion-color-danger-tint: #ed576b;
|
||||
|
||||
/** dark **/
|
||||
--ion-color-dark: #222428;
|
||||
--ion-color-dark-rgb: 34, 36, 40;
|
||||
--ion-color-dark-contrast: #ffffff;
|
||||
--ion-color-dark-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-dark-shade: #1e2023;
|
||||
--ion-color-dark-tint: #383a3e;
|
||||
|
||||
/** medium **/
|
||||
--ion-color-medium: #92949c;
|
||||
--ion-color-medium-rgb: 146, 148, 156;
|
||||
--ion-color-medium-contrast: #ffffff;
|
||||
--ion-color-medium-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-medium-shade: #808289;
|
||||
--ion-color-medium-tint: #9d9fa6;
|
||||
|
||||
/** light **/
|
||||
--ion-color-light: #f4f5f8;
|
||||
--ion-color-light-rgb: 244, 245, 248;
|
||||
--ion-color-light-contrast: #000000;
|
||||
--ion-color-light-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-light-shade: #d7d8da;
|
||||
--ion-color-light-tint: #f5f6f9;
|
||||
|
||||
--ion-color-medium2: var(--ion-color-medium);
|
||||
|
||||
--lightroom-bg: rgba(0, 0, 0, 0.047);
|
||||
|
||||
--thick-separator-color: var(--ion-background-color-step-50, #f2f2f7);
|
||||
|
||||
--ion-background-color-step-100: #f3f3f3;
|
||||
|
||||
--unread-item-background-color: #e3f1ff;
|
||||
|
||||
--ion-color-text-aside: rgba(0, 0, 0, 0.55);
|
||||
|
||||
--read-color: rgba(0, 0, 0, 0.45);
|
||||
--read-color-medium: rgba(0, 0, 0, 0.4);
|
||||
|
||||
--share-img-drop-shadow: none;
|
||||
|
||||
--ion-color-reddit-upvote: #ff5c01;
|
||||
|
||||
&.theme-has-custom-background {
|
||||
&.ios body,
|
||||
&.md body {
|
||||
--ion-background-color: var(--app-background) !important;
|
||||
--ion-item-background: var(--app-background);
|
||||
--ion-background-color-step-50: var(--app-inset-item-background);
|
||||
--ion-background-color-step-100: var(--app-tab-bar-background);
|
||||
--ion-tab-bar-background: var(--app-tab-bar-background);
|
||||
--ion-toolbar-background: var(--app-tab-bar-background);
|
||||
--thick-separator-color: var(--app-inset-item-background);
|
||||
}
|
||||
|
||||
ion-router-outlet ion-list.list-inset ion-item {
|
||||
--background: var(--app-inset-item-background);
|
||||
}
|
||||
|
||||
&.ios ion-modal:not(.transparent-scroll) {
|
||||
--ion-background-color: var(--app-background);
|
||||
--ion-toolbar-background: var(--app-tab-bar-background);
|
||||
--ion-toolbar-border-color: var(--ion-background-color-step-150);
|
||||
|
||||
--ion-item-background: var(--ion-background-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ios body {
|
||||
--ion-background-color: #fff;
|
||||
}
|
||||
|
||||
.ios ion-modal {
|
||||
--ion-background-color: var(--ion-background-color-step-50, #f2f2f7);
|
||||
--ion-item-background: var(--app-background, #fff);
|
||||
}
|
||||
|
||||
.ion-color-primary-fixed {
|
||||
--ion-color-base: var(--ion-color-primary-fixed);
|
||||
}
|
||||
.ion-color-reddit-upvote {
|
||||
--ion-color-base: var(--ion-color-reddit-upvote);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const lightVariables = css`
|
||||
:global() {
|
||||
html:not(.ion-palette-dark) {
|
||||
&:root {
|
||||
--ion-color-primary: var(--app-primary);
|
||||
--ion-color-primary-fixed: #3880ff; // always blue always blue!
|
||||
|
||||
ion-item {
|
||||
// default ionic light mode opacity is too harsh
|
||||
--background-activated-opacity: 0.06;
|
||||
}
|
||||
|
||||
&.ios .grey-bg {
|
||||
--ion-background-color: var(--ion-background-color-step-50, #f2f2f7);
|
||||
|
||||
ion-header {
|
||||
--opacity: 0;
|
||||
}
|
||||
ion-modal ion-content {
|
||||
--background: #fff;
|
||||
}
|
||||
ion-item {
|
||||
--ion-background-color: #fff;
|
||||
}
|
||||
ion-item-sliding {
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&.theme-has-custom-background {
|
||||
ion-router-outlet .grey-bg ion-list.list-inset ion-item {
|
||||
--background: var(--app-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const darkVariables = css`
|
||||
:global() {
|
||||
html.ion-palette-dark {
|
||||
// Dark Colors
|
||||
&:root {
|
||||
--ion-color-primary-fixed: #428cff; // always blue always blue!
|
||||
|
||||
--lightroom-bg: rgba(255, 255, 255, 0.08);
|
||||
|
||||
--ion-item-border-color: #333;
|
||||
}
|
||||
|
||||
body {
|
||||
--ion-color-primary: var(--app-primary);
|
||||
--ion-color-primary-rgb: 66, 140, 255;
|
||||
--ion-color-primary-contrast: #ffffff;
|
||||
--ion-color-primary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-primary-shade: #3a7be0;
|
||||
--ion-color-primary-tint: #5598ff;
|
||||
|
||||
--ion-color-secondary: #50c8ff;
|
||||
--ion-color-secondary-rgb: 80, 200, 255;
|
||||
--ion-color-secondary-contrast: #ffffff;
|
||||
--ion-color-secondary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-secondary-shade: #46b0e0;
|
||||
--ion-color-secondary-tint: #62ceff;
|
||||
|
||||
--ion-color-tertiary: #6a64ff;
|
||||
--ion-color-tertiary-rgb: 106, 100, 255;
|
||||
--ion-color-tertiary-contrast: #ffffff;
|
||||
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-tertiary-shade: #5d58e0;
|
||||
--ion-color-tertiary-tint: #7974ff;
|
||||
|
||||
--ion-color-success: #00940c;
|
||||
--ion-color-success-rgb: 0, 148, 12;
|
||||
--ion-color-success-contrast: #ffffff;
|
||||
--ion-color-success-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-success-shade: #00820b;
|
||||
--ion-color-success-tint: #1a9f24;
|
||||
|
||||
--ion-color-warning: #eac200;
|
||||
--ion-color-warning-rgb: 234, 194, 0;
|
||||
--ion-color-warning-contrast: #000000;
|
||||
--ion-color-warning-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-warning-shade: #ceab00;
|
||||
--ion-color-warning-tint: #ecc81a;
|
||||
|
||||
--ion-color-danger: #ff4961;
|
||||
--ion-color-danger-rgb: 255, 73, 97;
|
||||
--ion-color-danger-contrast: #ffffff;
|
||||
--ion-color-danger-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-danger-shade: #e04055;
|
||||
--ion-color-danger-tint: #ff5b71;
|
||||
|
||||
--ion-color-dark: #f4f5f8;
|
||||
--ion-color-dark-rgb: 244, 245, 248;
|
||||
--ion-color-dark-contrast: #000000;
|
||||
--ion-color-dark-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-dark-shade: #d7d8da;
|
||||
--ion-color-dark-tint: #f5f6f9;
|
||||
|
||||
--ion-color-medium: #989aa2;
|
||||
--ion-color-medium-rgb: 152, 154, 162;
|
||||
--ion-color-medium-contrast: #000000;
|
||||
--ion-color-medium-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-medium-shade: #86888f;
|
||||
--ion-color-medium-tint: #a2a4ab;
|
||||
|
||||
--ion-color-medium2: rgb(112, 113, 120);
|
||||
|
||||
--thick-separator-color: rgba(255, 255, 255, 0.08);
|
||||
|
||||
--unread-item-background-color: #162f4a;
|
||||
|
||||
--ion-color-text-aside: rgba(255, 255, 255, 0.65);
|
||||
|
||||
--read-color: rgba(255, 255, 255, 0.6);
|
||||
--read-color-medium: rgba(255, 255, 255, 0.4);
|
||||
|
||||
--share-img-drop-shadow: drop-shadow(0 0 8px black);
|
||||
|
||||
--ion-color-reddit-upvote: #f26700;
|
||||
}
|
||||
|
||||
// iOS Dark Theme
|
||||
|
||||
&.ios body {
|
||||
/* --ion-text-color: #ddd;
|
||||
--ion-text-color-rgb: 255, 255, 255; */
|
||||
|
||||
--ion-item-background: var(--ion-background-color);
|
||||
|
||||
--ion-card-background: #1c1c1d;
|
||||
|
||||
--ion-toolbar-background: var(--ion-background-color);
|
||||
}
|
||||
|
||||
&.ios ion-modal {
|
||||
--ion-background-color: var(--ion-background-color-step-100);
|
||||
--ion-toolbar-background: var(--ion-background-color-step-150);
|
||||
--ion-toolbar-border-color: var(--ion-background-color-step-250);
|
||||
--ion-item-background: var(--ion-background-color-step-50);
|
||||
}
|
||||
|
||||
// Material Design Dark Theme
|
||||
|
||||
@media (max-width: 767px) {
|
||||
&.ios ion-modal:not(.small, .transparent-scroll) {
|
||||
--ion-background-color: #000;
|
||||
--ion-toolbar-background: var(--ion-background-color);
|
||||
--ion-toolbar-border-color: var(--ion-background-color-step-150);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO test other themes
|
||||
ion-modal.transparent-scroll.dark {
|
||||
--ion-background-color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const darkBlackModifierVariables = css`
|
||||
:global() {
|
||||
html.ion-palette-dark {
|
||||
&:not(.theme-pure-black) {
|
||||
&.ios {
|
||||
body {
|
||||
--ion-background-color: #22252f;
|
||||
--ion-background-color-rgb: 34, 37, 47;
|
||||
--ion-tab-bar-background: rgba(0, 0, 0, 0.2);
|
||||
--ion-toolbar-border-color: #444;
|
||||
|
||||
ion-tab-bar {
|
||||
--ion-tab-bar-background: var(--ion-background-color);
|
||||
--ion-tab-bar-border-color: #444;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.md {
|
||||
body {
|
||||
--ion-background-color: #1e1e1e;
|
||||
--ion-background-color-rgb: 18, 18, 18;
|
||||
|
||||
--ion-text-color: #ffffff;
|
||||
--ion-text-color-rgb: 255, 255, 255;
|
||||
|
||||
--ion-border-color: #222222;
|
||||
|
||||
--ion-item-background: #1e1e1e;
|
||||
|
||||
--ion-toolbar-background: #1f1f1f;
|
||||
|
||||
--ion-tab-bar-background: #1f1f1f;
|
||||
|
||||
--ion-card-background: #1e1e1e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.theme-pure-black {
|
||||
&.ios {
|
||||
body {
|
||||
--ion-background-color: #000000;
|
||||
--ion-background-color-rgb: 0, 0, 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.theme-has-custom-background) {
|
||||
&.md {
|
||||
body {
|
||||
--ion-item-background: black;
|
||||
--ion-toolbar-background: #121212;
|
||||
--ion-tab-bar-background: #121212;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.md {
|
||||
body {
|
||||
--ion-background-color: black;
|
||||
--ion-background-color-rgb: 18, 18, 18;
|
||||
|
||||
--ion-text-color: #ffffff;
|
||||
--ion-text-color-rgb: 255, 255, 255;
|
||||
|
||||
--ion-border-color: #222222;
|
||||
|
||||
--ion-card-background: black;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
Reference in New Issue
Block a user