Merge pull request 'feat: update info action sheet menu to match the plugins one and use Checkboxes for Options menu in Themes page' (#25) from kmmiio99o/Kettu:dev into dev
Reviewed-on: https://codeberg.org/cocobo1/Kettu/pulls/25
This commit is contained in:
commit
8c6932c193
3 changed files with 255 additions and 101 deletions
|
|
@ -1,83 +1,48 @@
|
|||
import { formatString, Strings } from "@core/i18n";
|
||||
import AddonCard, { CardWrapper } from "@core/ui/components/AddonCard";
|
||||
import { showConfirmationAlert } from "@core/vendetta/alerts";
|
||||
import { useProxy } from "@core/vendetta/storage";
|
||||
import { fetchTheme, removeTheme, selectTheme, themes, VdThemeInfo } from "@lib/addons/themes";
|
||||
import { VdThemeInfo, themes, selectTheme } from "@lib/addons/themes";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { settings } from "@lib/api/settings";
|
||||
import { clipboard } from "@metro/common";
|
||||
import { showToast } from "@ui/toasts";
|
||||
|
||||
function selectAndApply(value: boolean, theme: VdThemeInfo) {
|
||||
try {
|
||||
selectTheme(value ? theme : null);
|
||||
} catch (e: any) {
|
||||
console.error("Error while selectAndApply,", e);
|
||||
}
|
||||
}
|
||||
import { showSheet } from "@lib/ui/sheets";
|
||||
import { NavigationNative, React } from "@metro/common";
|
||||
|
||||
export default function ThemeCard({ item: theme }: CardWrapper<VdThemeInfo>) {
|
||||
useProxy(theme);
|
||||
const navigation = NavigationNative.useNavigation();
|
||||
const [removed, setRemoved] = React.useState(false);
|
||||
|
||||
const [removed, setRemoved] = React.useState(false);
|
||||
// This is needed because of React™
|
||||
if (removed) return null;
|
||||
|
||||
// This is needed because of React™
|
||||
if (removed) return null;
|
||||
const { authors } = theme.data;
|
||||
|
||||
const { authors } = theme.data;
|
||||
|
||||
return (
|
||||
<AddonCard
|
||||
headerLabel={theme.data.name}
|
||||
headerSublabel={authors ? `by ${authors.map(i => i.name).join(", ")}` : ""}
|
||||
descriptionLabel={theme.data.description ?? "No description."}
|
||||
toggleType={!settings.safeMode?.enabled ? "radio" : undefined}
|
||||
toggleValue={() => themes[theme.id].selected}
|
||||
onToggleChange={(v: boolean) => {
|
||||
selectAndApply(v, theme);
|
||||
}}
|
||||
overflowTitle={theme.data.name}
|
||||
overflowActions={[
|
||||
{
|
||||
icon: "ic_sync_24px",
|
||||
label: Strings.REFETCH,
|
||||
onPress: () => {
|
||||
fetchTheme(theme.id, theme.selected).then(() => {
|
||||
showToast(Strings.THEME_REFETCH_SUCCESSFUL, findAssetId("toast_image_saved"));
|
||||
}).catch(() => {
|
||||
showToast(Strings.THEME_REFETCH_FAILED, findAssetId("Small"));
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: "copy",
|
||||
label: Strings.COPY_URL,
|
||||
onPress: () => {
|
||||
clipboard.setString(theme.id);
|
||||
showToast.showCopyToClipboard();
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: "ic_message_delete",
|
||||
label: Strings.DELETE,
|
||||
isDestructive: true,
|
||||
onPress: () => showConfirmationAlert({
|
||||
title: Strings.HOLD_UP,
|
||||
content: formatString("ARE_YOU_SURE_TO_DELETE_THEME", { name: theme.data.name }),
|
||||
confirmText: Strings.DELETE,
|
||||
cancelText: Strings.CANCEL,
|
||||
confirmColor: "red",
|
||||
onConfirm: () => {
|
||||
removeTheme(theme.id).then(wasSelected => {
|
||||
setRemoved(true);
|
||||
if (wasSelected) selectAndApply(false, theme);
|
||||
}).catch((e: Error) => {
|
||||
showToast(e.message, findAssetId("Small"));
|
||||
});
|
||||
}
|
||||
})
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<AddonCard
|
||||
headerLabel={theme.data.name}
|
||||
headerSublabel={
|
||||
authors ? `by ${authors.map((i) => i.name).join(", ")}` : ""
|
||||
}
|
||||
descriptionLabel={theme.data.description ?? "No description."}
|
||||
toggleType={!settings.safeMode?.enabled ? "radio" : undefined}
|
||||
toggleValue={() => themes[theme.id].selected}
|
||||
onToggleChange={(v: boolean) => {
|
||||
try {
|
||||
selectTheme(v ? theme : null);
|
||||
} catch (e: any) {
|
||||
console.error("Error while selecting theme:", e);
|
||||
}
|
||||
}}
|
||||
overflowTitle={theme.data.name}
|
||||
actions={[
|
||||
{
|
||||
icon: "CircleInformationIcon-primary",
|
||||
onPress: () => {
|
||||
const importPromise = import("./sheets/ThemeInfoActionSheet");
|
||||
showSheet("ThemeInfoActionSheet", importPromise, {
|
||||
theme,
|
||||
navigation,
|
||||
});
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import {
|
|||
BottomSheetTitleHeader,
|
||||
Button,
|
||||
TableRowGroup,
|
||||
TableSwitchRow,
|
||||
TableCheckboxRow,
|
||||
TableRowIcon,
|
||||
} from "@metro/common/components";
|
||||
import { View } from "react-native";
|
||||
|
|
@ -74,14 +74,12 @@ export default function Themes() {
|
|||
{/* Changed from TableRadioGroup to individual TableSwitchRow components
|
||||
for better UX - users can now toggle options individually */}
|
||||
<TableRowGroup title="Override Theme Type">
|
||||
<TableSwitchRow
|
||||
<TableCheckboxRow
|
||||
label="Auto"
|
||||
icon={<TableRowIcon source={findAssetId("RobotIcon")} />}
|
||||
value={!colorsPref.type}
|
||||
onValueChange={(enabled: boolean) => {
|
||||
if (enabled) {
|
||||
colorsPref.type = undefined;
|
||||
} else {
|
||||
checked={!colorsPref.type}
|
||||
onPress={() => {
|
||||
if (!colorsPref.type) {
|
||||
colorsPref.type = "dark";
|
||||
}
|
||||
getCurrentTheme()?.data &&
|
||||
|
|
@ -90,24 +88,24 @@ export default function Themes() {
|
|||
});
|
||||
}}
|
||||
/>
|
||||
<TableSwitchRow
|
||||
<TableCheckboxRow
|
||||
label="Dark"
|
||||
icon={<TableRowIcon source={findAssetId("ThemeDarkIcon")} />}
|
||||
value={colorsPref.type === "dark"}
|
||||
onValueChange={(enabled: boolean) => {
|
||||
colorsPref.type = enabled ? "dark" : undefined;
|
||||
checked={colorsPref.type === "dark"}
|
||||
onPress={() => {
|
||||
colorsPref.type = colorsPref.type === "dark" ? undefined : "dark";
|
||||
getCurrentTheme()?.data &&
|
||||
updateBunnyColor(getCurrentTheme()!.data!, {
|
||||
update: true,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<TableSwitchRow
|
||||
<TableCheckboxRow
|
||||
label="Light"
|
||||
icon={<TableRowIcon source={findAssetId("ThemeLightIcon")} />}
|
||||
value={colorsPref.type === "light"}
|
||||
onValueChange={(enabled: boolean) => {
|
||||
colorsPref.type = enabled ? "light" : undefined;
|
||||
checked={colorsPref.type === "light"}
|
||||
onPress={() => {
|
||||
colorsPref.type = colorsPref.type === "light" ? undefined : "light";
|
||||
getCurrentTheme()?.data &&
|
||||
updateBunnyColor(getCurrentTheme()!.data!, {
|
||||
update: true,
|
||||
|
|
@ -116,20 +114,13 @@ export default function Themes() {
|
|||
/>
|
||||
</TableRowGroup>
|
||||
<TableRowGroup title="Chat Background">
|
||||
<TableSwitchRow
|
||||
<TableCheckboxRow
|
||||
label="Show Background"
|
||||
subLabel="Enable or disable themes background on chat"
|
||||
icon={<TableRowIcon source={findAssetId("ImageIcon")} />}
|
||||
value={!colorsPref.customBackground}
|
||||
onValueChange={(enabled: boolean) => {
|
||||
colorsPref.customBackground = enabled ? null : "hidden";
|
||||
}}
|
||||
/>
|
||||
<TableSwitchRow
|
||||
label="Hide Background"
|
||||
icon={<TableRowIcon source={findAssetId("DenyIcon")} />}
|
||||
value={colorsPref.customBackground === "hidden"}
|
||||
onValueChange={(enabled: boolean) => {
|
||||
colorsPref.customBackground = enabled ? "hidden" : null;
|
||||
checked={!colorsPref.customBackground}
|
||||
onPress={() => {
|
||||
colorsPref.customBackground = !colorsPref.customBackground ? "hidden" : null;
|
||||
}}
|
||||
/>
|
||||
</TableRowGroup>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,198 @@
|
|||
import { findAssetId } from "@lib/api/assets";
|
||||
import { purgeStorage } from "@lib/api/storage";
|
||||
import { hideSheet } from "@lib/ui/sheets";
|
||||
import { ActionSheet, Card, IconButton, Text } from "@metro/common/components";
|
||||
import { clipboard } from "@metro/common";
|
||||
import React, { ComponentProps, useEffect, useState } from "react";
|
||||
import { ScrollView, View, TouchableOpacity } from "react-native";
|
||||
import { showToast } from "@ui/toasts";
|
||||
import { showConfirmationAlert } from "@core/vendetta/alerts";
|
||||
import { fetchTheme, removeTheme, selectTheme, VdThemeInfo, } from "@lib/addons/themes";
|
||||
import { formatString, Strings } from "@core/i18n";
|
||||
import { semanticColors } from "@ui/color";
|
||||
|
||||
interface ThemeInfoActionSheetProps {
|
||||
theme: VdThemeInfo;
|
||||
navigation: any;
|
||||
}
|
||||
|
||||
function ThemeInfoIconButton(props: ComponentProps<typeof IconButton>) {
|
||||
const { onPress } = props;
|
||||
props.onPress &&= () => {
|
||||
hideSheet("ThemeInfoActionSheet");
|
||||
onPress?.();
|
||||
};
|
||||
|
||||
return <IconButton {...props} label={props.label} />;
|
||||
}
|
||||
|
||||
// theme title component for header
|
||||
function TitleComponent({ theme }: { theme: VdThemeInfo }) {
|
||||
const { authors } = theme.data;
|
||||
|
||||
return (
|
||||
<View style={{ gap: 4 }}>
|
||||
<View>
|
||||
<Text variant="heading-xl/semibold">{theme.data.name}</Text>
|
||||
</View>
|
||||
<View style={{ flexDirection: "row", flexShrink: 1 }}>
|
||||
{authors && authors.length > 0 && (
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
gap: 8,
|
||||
alignItems: "center",
|
||||
paddingVertical: 4,
|
||||
paddingHorizontal: 8,
|
||||
backgroundColor: "#00000016",
|
||||
borderRadius: 32,
|
||||
}}
|
||||
disabled={!authors.some((a) => a.id)}
|
||||
>
|
||||
<Text variant="text-md/medium">
|
||||
by {authors.map((a) => a.name).join(", ")}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ThemeInfoActionSheet({
|
||||
theme,
|
||||
navigation,
|
||||
}: ThemeInfoActionSheetProps) {
|
||||
// use component state to track theme data
|
||||
const [themeState, setThemeState] = useState({ ...theme });
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// periodic refresh to detect external changes to theme state
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setThemeState({ ...theme });
|
||||
}, 500);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [theme]);
|
||||
|
||||
const copyThemeUrl = () => {
|
||||
// Copy theme ID as URL
|
||||
clipboard.setString(themeState.id);
|
||||
if (typeof showToast?.showCopyToClipboard === "function") {
|
||||
showToast.showCopyToClipboard();
|
||||
} else {
|
||||
showToast("Copied to clipboard");
|
||||
}
|
||||
};
|
||||
|
||||
const refetchTheme = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await fetchTheme(themeState.id, themeState.selected);
|
||||
showToast("Theme refreshed successfully");
|
||||
} catch (e) {
|
||||
console.error("Failed to refresh theme:", e);
|
||||
showToast("Failed to refresh theme");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const removeThemeHandler = () => {
|
||||
showConfirmationAlert({
|
||||
title: Strings.HOLD_UP,
|
||||
content: formatString("ARE_YOU_SURE_TO_DELETE_THEME", {
|
||||
name: themeState.data.name,
|
||||
}),
|
||||
confirmText: Strings.DELETE,
|
||||
cancelText: Strings.CANCEL,
|
||||
confirmColor: "red",
|
||||
onConfirm: async () => {
|
||||
hideSheet("ThemeInfoActionSheet");
|
||||
try {
|
||||
const wasSelected = await removeTheme(themeState.id);
|
||||
if (wasSelected) selectTheme(null);
|
||||
showToast("Theme removed successfully");
|
||||
} catch (e) {
|
||||
console.error("Failed to remove theme:", e);
|
||||
showToast("Failed to remove theme");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const applyTheme = () => {
|
||||
try {
|
||||
selectTheme(themeState);
|
||||
hideSheet("ThemeInfoActionSheet");
|
||||
showToast(`Applied theme: ${themeState.data.name}`);
|
||||
} catch (e) {
|
||||
console.error("Failed to apply theme:", e);
|
||||
showToast("Failed to apply theme");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ActionSheet>
|
||||
<ScrollView contentContainerStyle={{ gap: 12, marginBottom: 12 }}>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
paddingVertical: 24,
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<TitleComponent theme={themeState} />
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
flexWrap: "wrap",
|
||||
gap: 65,
|
||||
paddingHorizontal: 8,
|
||||
}}
|
||||
>
|
||||
<ThemeInfoIconButton
|
||||
label={Strings.REFETCH}
|
||||
variant="secondary"
|
||||
icon={findAssetId("RetryIcon")}
|
||||
onPress={refetchTheme}
|
||||
disabled={loading}
|
||||
/>
|
||||
<ThemeInfoIconButton
|
||||
label={Strings.COPY_URL}
|
||||
variant="secondary"
|
||||
icon={findAssetId("LinkIcon")}
|
||||
onPress={copyThemeUrl}
|
||||
/>
|
||||
<ThemeInfoIconButton
|
||||
label={Strings.UNINSTALL}
|
||||
variant="secondary"
|
||||
icon={findAssetId("TrashIcon")}
|
||||
onPress={removeThemeHandler}
|
||||
/>
|
||||
</View>
|
||||
<Card>
|
||||
<Text
|
||||
variant="text-md/semibold"
|
||||
style={{
|
||||
marginBottom: 4,
|
||||
color: semanticColors.MOBILE_TEXT_HEADING_PRIMARY,
|
||||
}}
|
||||
>
|
||||
Description
|
||||
</Text>
|
||||
<Text variant="text-md/medium">
|
||||
{themeState.data.description || "No description provided."}
|
||||
</Text>
|
||||
</Card>
|
||||
</ScrollView>
|
||||
</ActionSheet>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue