Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.agents/skills/
12 changes: 12 additions & 0 deletions app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ const config: ExpoConfig = {
},
predictiveBackGestureEnabled: false,
},
locales: {
en: "./assets/locales/app-meta-en.json",
},
plugins: [
...(IS_DEV ? ["expo-dev-client"] : []),
"expo-router",
Expand All @@ -69,6 +72,15 @@ const config: ExpoConfig = {
"expo-image",
"expo-secure-store",
"expo-sharing",
[
"expo-localization",
{
supportedLocales: {
ios: ["zh-Hans", "en"],
android: ["zh-Hans", "en"],
},
},
],
[
"expo-splash-screen",
{
Expand Down
57 changes: 32 additions & 25 deletions app/(pages)/about/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ import { MenuGroup, MenuItem } from "@/components/ui/menu-item";
import { IS_DEV } from "@/constants/is-dev";
import { Colors } from "@/constants/theme";
import { useColorScheme } from "@/hooks/use-color-scheme";
import { useT } from "@/lib/i18n";
import { useUpdateStore } from "@/store/update";

const icon = require("@/assets/images/icon.png");
const uniLabel = require("@/assets/images/icon_uni_label.svg");

export default function AboutScreen() {
const t = useT();
const scheme = useColorScheme();
const isDark = scheme === "dark";
const version = Constants.expoConfig?.version ?? "N/A";
Expand All @@ -33,14 +35,17 @@ export default function AboutScreen() {
const checking = useUpdateStore((s) => s.checking);
const check = useUpdateStore((s) => s.check);

const copyToClipboard = useCallback(async (label: string, value: string) => {
await Clipboard.setStringAsync(value);
Toast.show({
type: "success",
text1: `已复制${label}`,
position: "bottom",
});
}, []);
const copyToClipboard = useCallback(
async (label: string, value: string) => {
await Clipboard.setStringAsync(value);
Toast.show({
type: "success",
text1: t("about.copied", { label }),
position: "bottom",
});
},
[t],
);

const handleCheckUpdate = useCallback(async () => {
await check();
Expand All @@ -49,8 +54,8 @@ export default function AboutScreen() {
if (updated) {
Toast.show({
type: "info",
text1: "发现新版本",
text2: `v${latest} 可用,点击下载`,
text1: t("about.newVersionFound"),
text2: t("about.newVersionTip", { v: latest ?? "" }),
position: "bottom",
onPress: () => {
if (Platform.OS === "ios") {
Expand All @@ -67,15 +72,15 @@ export default function AboutScreen() {
} else {
Toast.show({
type: "success",
text1: "当前已是最新版本",
text1: t("about.upToDate"),
position: "bottom",
});
}
}, [check]);
}, [check, t]);

return (
<>
<Stack.Screen options={{ title: "关于" }} />
<Stack.Screen options={{ title: t("about.title") }} />
<ScrollView
className="flex-1 bg-neutral-100 dark:bg-neutral-900"
contentContainerClassName="px-4 pt-4 pb-8"
Expand All @@ -86,38 +91,40 @@ export default function AboutScreen() {
style={{ width: 80, height: 80, borderRadius: 18 }}
/>
<Text className="mt-4 text-xl font-semibold text-neutral-900 dark:text-neutral-100">
掌上吾理
{t("about.appName")}
</Text>
<Text className="mt-1.5 text-sm text-neutral-400 dark:text-neutral-500">
{version}
{IS_DEV ? " (Dev)" : ""}
</Text>
</View>

<MenuGroup title="信息">
<MenuGroup title={t("about.infoGroup")}>
<MenuItem
icon="info-outline"
label="版本号"
label={t("about.version")}
value={version}
showArrow={false}
onPress={() => copyToClipboard("版本号", version)}
onPress={() => copyToClipboard(t("about.version"), version)}
/>
<MenuItem
icon="commit"
label="Commit"
label={t("about.commit")}
value={commit}
showArrow={false}
onPress={() => copyToClipboard("Commit", commit)}
onPress={() => copyToClipboard(t("about.commit"), commit)}
/>
</MenuGroup>

<MenuGroup title="更新">
<MenuGroup title={t("about.updateGroup")}>
<MenuItem
icon="system-update"
iconBg="#34C759"
label="检查更新"
label={t("about.checkUpdate")}
value={
hasUpdate && latestVersion ? `新版本 ${latestVersion}` : undefined
hasUpdate && latestVersion
? t("about.newVersionAvailable", { v: latestVersion })
: undefined
}
badge={hasUpdate}
showArrow={false}
Expand All @@ -126,17 +133,17 @@ export default function AboutScreen() {
/>
</MenuGroup>

<MenuGroup title="链接">
<MenuGroup title={t("about.linksGroup")}>
<MenuItem
icon="language"
iconBg="#007AFF"
label="官方网站"
label={t("about.website")}
onPress={() => Linking.openURL("https://iwut.tokenteam.net")}
/>
<MenuItem
icon="code"
iconBg="#333"
label="GitHub"
label={t("about.github")}
onPress={() => Linking.openURL("https://github.com/tokenteam/iwut")}
/>
</MenuGroup>
Expand Down
100 changes: 85 additions & 15 deletions app/(pages)/settings/appearance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,44 +7,80 @@ import { IconSymbol } from "@/components/ui/icon-symbol";
import { MenuGroup, MenuItem } from "@/components/ui/menu-item";
import { Colors } from "@/constants/theme";
import { useColorScheme } from "@/hooks/use-color-scheme";
import { type Lang, useT } from "@/lib/i18n";
import { useSettingsStore } from "@/store/settings";
import { type ThemeMode, useThemeStore } from "@/store/theme";

const themeOptions: { mode: ThemeMode; icon: string; label: string }[] = [
{ mode: "system", icon: "brightness-auto", label: "跟随系统" },
{ mode: "light", icon: "light-mode", label: "浅色模式" },
{ mode: "dark", icon: "dark-mode", label: "深色模式" },
];

const themeLabelMap: Record<ThemeMode, string> = {
system: "跟随系统",
light: "浅色模式",
dark: "深色模式",
};

export default function AppearanceScreen() {
const t = useT();
const scheme = useColorScheme();
const iconColor = Colors[scheme === "dark" ? "dark" : "light"].icon;
const tintColor = Colors[scheme === "dark" ? "dark" : "light"].tint;
const themeMode = useThemeStore((s) => s.themeMode);
const setThemeMode = useThemeStore((s) => s.setThemeMode);
const language = useSettingsStore((s) => s.language);
const setLanguage = useSettingsStore((s) => s.setLanguage);
const [showThemePicker, setShowThemePicker] = useState(false);
const [showLangPicker, setShowLangPicker] = useState(false);

const themeOptions: { mode: ThemeMode; icon: string; label: string }[] = [
{
mode: "system",
icon: "brightness-auto",
label: t("appearance.themeSystem"),
},
{ mode: "light", icon: "light-mode", label: t("appearance.themeLight") },
{ mode: "dark", icon: "dark-mode", label: t("appearance.themeDark") },
];

const themeLabelMap: Record<ThemeMode, string> = {
system: t("appearance.themeSystem"),
light: t("appearance.themeLight"),
dark: t("appearance.themeDark"),
};

const langOptions: { lang: Lang; icon: string; label: string }[] = [
{
lang: "system",
icon: "brightness-auto",
label: t("appearance.langSystem"),
},
{ lang: "zh", icon: "translate", label: t("appearance.langZh") },
{ lang: "en", icon: "translate", label: t("appearance.langEn") },
];

const langLabelMap: Record<Lang, string> = {
system: t("appearance.langSystem"),
zh: t("appearance.langZh"),
en: t("appearance.langEn"),
};

return (
<>
<Stack.Screen options={{ title: "外观设置" }} />
<Stack.Screen options={{ title: t("appearance.title") }} />
<ScrollView
className="flex-1 bg-neutral-100 dark:bg-neutral-900"
contentContainerClassName="px-4 pt-4"
>
<MenuGroup title="主题">
<MenuGroup title={t("appearance.themeGroup")}>
<MenuItem
icon="palette"
iconBg="#5856D6"
label="主题"
label={t("appearance.theme")}
value={themeLabelMap[themeMode]}
onPress={() => setShowThemePicker(true)}
/>
</MenuGroup>

<MenuGroup title={t("appearance.languageGroup")}>
<MenuItem
icon="translate"
iconBg="#0d9488"
label={t("appearance.language")}
value={langLabelMap[language]}
onPress={() => setShowLangPicker(true)}
/>
</MenuGroup>
</ScrollView>

<BottomSheet
Expand Down Expand Up @@ -80,6 +116,40 @@ export default function AppearanceScreen() {
</Pressable>
))}
</BottomSheet>

<BottomSheet
visible={showLangPicker}
onClose={() => setShowLangPicker(false)}
>
{langOptions.map((opt) => (
<Pressable
key={opt.lang}
className="flex-row items-center px-5 py-3.5 active:bg-neutral-100 dark:active:bg-neutral-700"
onPress={() => {
setLanguage(opt.lang);
setShowLangPicker(false);
}}
>
<IconSymbol
name={opt.icon as any}
size={22}
color={language === opt.lang ? tintColor : iconColor}
/>
<Text
className={`ml-3 flex-1 text-base ${
language === opt.lang
? "font-medium text-sky-600 dark:text-white"
: "text-neutral-800 dark:text-neutral-200"
}`}
>
{opt.label}
</Text>
{language === opt.lang && (
<IconSymbol name="check" size={20} color={tintColor} />
)}
</Pressable>
))}
</BottomSheet>
</>
);
}
Loading
Loading