diff --git a/app/i18n/locales/ar.json b/app/i18n/locales/ar.json index 0c45784..edfa0bc 100644 --- a/app/i18n/locales/ar.json +++ b/app/i18n/locales/ar.json @@ -353,6 +353,7 @@ "settings.title": "الإعدادات", "settings.vibration": "الاهتزاز أثناء التصفح", "settings.vibrationDetail": "نبضة خفيفة عند التنقل والاختيار والرجوع", + "settings.vibrationOff": "إيقاف", "shadowMount.offAction": "تظهر اللعبة بعد تحميله. أضفه إلى التحميل التلقائي لكسر الحماية ليبقى يعمل دائمًا.", "shadowMount.offTitle": "ShadowMount لا يعمل", "sort.label": "ترتيب", diff --git a/app/i18n/locales/bn.json b/app/i18n/locales/bn.json index 133fb07..6a0260f 100644 --- a/app/i18n/locales/bn.json +++ b/app/i18n/locales/bn.json @@ -353,6 +353,7 @@ "settings.title": "সেটিংস", "settings.vibration": "ব্রাউজ করার সময় কম্পন", "settings.vibrationDetail": "সরানো, বেছে নেওয়া ও ফিরে যাওয়ার সময় হালকা কম্পন", + "settings.vibrationOff": "বন্ধ", "shadowMount.offAction": "এটি লোড করলে গেমটি দেখা যাবে। সবসময় চালু রাখতে জেলব্রেকের অটোলোডে যোগ করুন।", "shadowMount.offTitle": "ShadowMount চলছে না", "sort.label": "বাছাই করুন", diff --git a/app/i18n/locales/de.json b/app/i18n/locales/de.json index 532a2c9..fcc1bb8 100644 --- a/app/i18n/locales/de.json +++ b/app/i18n/locales/de.json @@ -353,6 +353,7 @@ "settings.title": "Einstellungen", "settings.vibration": "Vibration beim Navigieren", "settings.vibrationDetail": "Leichter Impuls beim Bewegen, Wählen und Zurück", + "settings.vibrationOff": "Aus", "shadowMount.offAction": "Das Spiel erscheint, sobald du es lädst. Füge es zum Autoload deines Jailbreaks hinzu, damit es immer läuft.", "shadowMount.offTitle": "ShadowMount läuft nicht", "sort.label": "Sortieren", diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index 4767f67..59e5a88 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -356,6 +356,7 @@ "settings.title": "Settings", "settings.vibration": "Vibration while browsing", "settings.vibrationDetail": "A light pulse as you move, select and go back", + "settings.vibrationOff": "Off", "shadowMount.offAction": "The game shows up once you load it. Add it to your jailbreak's autoload to keep it on.", "shadowMount.offTitle": "ShadowMount isn't running", "sort.label": "Sort", diff --git a/app/i18n/locales/es.json b/app/i18n/locales/es.json index 5c5e086..68f977d 100644 --- a/app/i18n/locales/es.json +++ b/app/i18n/locales/es.json @@ -353,6 +353,7 @@ "settings.title": "Ajustes", "settings.vibration": "Vibración al navegar", "settings.vibrationDetail": "Un leve pulso al moverte, seleccionar y volver", + "settings.vibrationOff": "Apagada", "shadowMount.offAction": "El juego aparece cuando lo cargues. Añádelo al autoload del jailbreak para que siempre esté activo.", "shadowMount.offTitle": "ShadowMount no se está ejecutando", "sort.label": "Ordenar", diff --git a/app/i18n/locales/fa.json b/app/i18n/locales/fa.json index 5d3d0b9..93eb9ea 100644 --- a/app/i18n/locales/fa.json +++ b/app/i18n/locales/fa.json @@ -354,6 +354,7 @@ "settings.title": "تنظیمات", "settings.vibration": "لرزش هنگام مرور", "settings.vibrationDetail": "ضربهٔ ملایم هنگام حرکت، انتخاب و بازگشت", + "settings.vibrationOff": "خاموش", "shadowMount.offAction": "بازی وقتی بارگذاری شود ظاهر می‌شود. آن را به autoload جیلبریک اضافه کنید تا روشن بماند.", "shadowMount.offTitle": "ShadowMount در حال اجرا نیست", "sort.label": "مرتب‌سازی", diff --git a/app/i18n/locales/fr.json b/app/i18n/locales/fr.json index 3398fe5..bd29b2a 100644 --- a/app/i18n/locales/fr.json +++ b/app/i18n/locales/fr.json @@ -353,6 +353,7 @@ "settings.title": "Réglages", "settings.vibration": "Vibration pendant la navigation", "settings.vibrationDetail": "Une légère impulsion quand vous vous déplacez, sélectionnez ou revenez", + "settings.vibrationOff": "Désactivée", "shadowMount.offAction": "Le jeu apparaît une fois ShadowMount chargé. Ajoutez-le à l'autoload du jailbreak pour qu'il reste actif.", "shadowMount.offTitle": "ShadowMount n'est pas lancé", "sort.label": "Trier", diff --git a/app/i18n/locales/hi.json b/app/i18n/locales/hi.json index 9020558..98f1363 100644 --- a/app/i18n/locales/hi.json +++ b/app/i18n/locales/hi.json @@ -353,6 +353,7 @@ "settings.title": "सेटिंग्स", "settings.vibration": "ब्राउज़ करते समय वाइब्रेशन", "settings.vibrationDetail": "आगे बढ़ने, चुनने और वापस जाने पर हल्का कंपन", + "settings.vibrationOff": "बंद", "shadowMount.offAction": "इसे लोड करने पर गेम दिखेगा। इसे हमेशा चालू रखने के लिए जेलब्रेक के ऑटोलोड में जोड़ें।", "shadowMount.offTitle": "ShadowMount नहीं चल रहा", "sort.label": "क्रमबद्ध करें", diff --git a/app/i18n/locales/id.json b/app/i18n/locales/id.json index 7b8075f..264eb5d 100644 --- a/app/i18n/locales/id.json +++ b/app/i18n/locales/id.json @@ -353,6 +353,7 @@ "settings.title": "Pengaturan", "settings.vibration": "Getaran saat menjelajah", "settings.vibrationDetail": "Getaran ringan saat berpindah, memilih, dan kembali", + "settings.vibrationOff": "Mati", "shadowMount.offAction": "Game muncul setelah kamu memuatnya. Tambahkan ke autoload jailbreak agar selalu aktif.", "shadowMount.offTitle": "ShadowMount tidak berjalan", "sort.label": "Urutkan", diff --git a/app/i18n/locales/it.json b/app/i18n/locales/it.json index 932685e..f566771 100644 --- a/app/i18n/locales/it.json +++ b/app/i18n/locales/it.json @@ -361,6 +361,7 @@ "settings.title": "Impostazioni", "settings.vibration": "Vibrazione durante la navigazione", "settings.vibrationDetail": "Un leggero impulso quando ti sposti, selezioni e torni indietro", + "settings.vibrationOff": "Spenta", "shadowMount.offAction": "Il gioco compare quando lo carichi. Aggiungilo all'autoload del tuo jailbreak per tenerlo sempre attivo.", "shadowMount.offTitle": "ShadowMount non è attivo", "sort.label": "Ordina", diff --git a/app/i18n/locales/ja.json b/app/i18n/locales/ja.json index c9e36b3..acfdf18 100644 --- a/app/i18n/locales/ja.json +++ b/app/i18n/locales/ja.json @@ -353,6 +353,7 @@ "settings.title": "設定", "settings.vibration": "操作時の振動", "settings.vibrationDetail": "移動、選択、戻る操作のときに軽く振動します", + "settings.vibrationOff": "オフ", "shadowMount.offAction": "ShadowMount を読み込むとゲームが表示されます。脱獄の自動読み込みに追加すると常に起動します。", "shadowMount.offTitle": "ShadowMount が起動していません", "sort.label": "並べ替え", diff --git a/app/i18n/locales/pt-BR.json b/app/i18n/locales/pt-BR.json index e5e99f1..bb890ac 100644 --- a/app/i18n/locales/pt-BR.json +++ b/app/i18n/locales/pt-BR.json @@ -353,6 +353,7 @@ "settings.title": "Configurações", "settings.vibration": "Vibração ao navegar", "settings.vibrationDetail": "Um pulso leve ao mover, selecionar e voltar", + "settings.vibrationOff": "Desligada", "shadowMount.offAction": "O jogo aparece quando você carregar ele. Coloque no autoload do jailbreak pra ficar sempre ligado.", "shadowMount.offTitle": "O ShadowMount não está rodando", "sort.label": "Ordenar", diff --git a/app/i18n/locales/ru.json b/app/i18n/locales/ru.json index 2b47960..b903883 100644 --- a/app/i18n/locales/ru.json +++ b/app/i18n/locales/ru.json @@ -353,6 +353,7 @@ "settings.title": "Настройки", "settings.vibration": "Вибрация при навигации", "settings.vibrationDetail": "Лёгкий отклик при перемещении, выборе и возврате", + "settings.vibrationOff": "Выкл.", "shadowMount.offAction": "Игра появится, когда вы его загрузите. Добавьте его в автозагрузку джейлбрейка, чтобы он всегда работал.", "shadowMount.offTitle": "ShadowMount не запущен", "sort.label": "Сортировка", diff --git a/app/i18n/locales/ur.json b/app/i18n/locales/ur.json index acbc033..80f2f91 100644 --- a/app/i18n/locales/ur.json +++ b/app/i18n/locales/ur.json @@ -353,6 +353,7 @@ "settings.title": "ترتیبات", "settings.vibration": "براؤز کرتے وقت وائبریشن", "settings.vibrationDetail": "حرکت، انتخاب اور واپس جانے پر ہلکی سی وائبریشن", + "settings.vibrationOff": "بند", "shadowMount.offAction": "اسے لوڈ کرنے پر گیم نظر آئے گی۔ ہمیشہ چلانے کے لیے جیل بریک کے آٹو لوڈ میں شامل کریں۔", "shadowMount.offTitle": "ShadowMount نہیں چل رہا", "sort.label": "ترتیب دیں", diff --git a/app/i18n/locales/zh-Hans.json b/app/i18n/locales/zh-Hans.json index 3aeea6e..abb0f16 100644 --- a/app/i18n/locales/zh-Hans.json +++ b/app/i18n/locales/zh-Hans.json @@ -353,6 +353,7 @@ "settings.title": "设置", "settings.vibration": "浏览时振动", "settings.vibrationDetail": "移动、选择和返回时轻微振动", + "settings.vibrationOff": "关闭", "shadowMount.offAction": "加载 ShadowMount 后游戏才会出现。将它加入越狱的自动加载即可保持运行。", "shadowMount.offTitle": "ShadowMount 未运行", "sort.label": "排序", diff --git a/app/ui/LanguageSheet.jsx b/app/ui/LanguageSheet.jsx index 6b39498..778ac6a 100644 --- a/app/ui/LanguageSheet.jsx +++ b/app/ui/LanguageSheet.jsx @@ -4,13 +4,14 @@ import '../assets/Inter-Regular.ttf'; import { Fragment, jsx, jsxs } from 'react/jsx-runtime'; +import { formatPercent2 } from '../i18n/core.js'; import { chooseLanguage, languageChoice, systemLocale, useT } from '../i18n/index.js'; import { LOCALES } from '../i18n/locales.js'; import { FocusRing } from './FocusRing.jsx'; import { Icon } from './Icon.jsx'; import { SideSheet } from './SideSheet.jsx'; import { colors } from './colors.js'; -import { setNavigationHaptics, useNavigationHaptics } from './haptics.js'; +import { LEVELS, setVibrationLevel, useVibrationLevel } from './haptics.js'; import { LIMITS, setSpeedLimit, useSpeedLimit } from '../state/speed-limit.js'; import { Text, View, useIsFocused } from '@ps5-react/core'; @@ -52,6 +53,29 @@ import { Text, View, useIsFocused } from '@ps5-react/core'; /* @__PURE__ */ (0, jsx)(View, { style: { flexDirection: "row", gap: 9 }, children: LIMITS.map((value) => /* @__PURE__ */ (0, jsx)(SpeedSegment, { limit: value, chosen: value === limit }, value)) }) ] }); } + // The vibration level (issue #32), laid out like the speed limit: every step in view, Cross picks. + var levelName = (t2, level) => level ? formatPercent2(level) : t2("settings.vibrationOff"); + function VibrationSegment({ level, chosen }) { + const t2 = useT(); + return /* @__PURE__ */ (0, jsxs)(View, { focusKey: `settings:vibration:${level}`, onPress: () => setVibrationLevel(level), style: { position: "relative", flex: level ? 1 : 2, height: 60, alignItems: "center", justifyContent: "center", borderRadius: 12, backgroundColor: chosen ? colors.fg : colors.surface2 }, children: [ + /* @__PURE__ */ (0, jsx)(FocusRing, {}), + /* @__PURE__ */ (0, jsx)(Text, { style: { fontFamily: "Inter-Regular", fontSize: 21, fontWeight: 600, color: chosen ? colors.accentFg : colors.muted, numberOfLines: 1 }, children: levelName(t2, level) }) + ] }); + } + function VibrationLevel() { + const t2 = useT(); + const level = useVibrationLevel(); + return /* @__PURE__ */ (0, jsxs)(View, { style: { gap: 18, paddingHorizontal: 18, paddingTop: 6, paddingBottom: 18 }, children: [ + /* @__PURE__ */ (0, jsxs)(View, { style: { gap: 3 }, children: [ + /* @__PURE__ */ (0, jsxs)(View, { style: { flexDirection: "row", justifyContent: "space-between", alignItems: "center" }, children: [ + /* @__PURE__ */ (0, jsx)(Text, { style: { fontFamily: "Inter-Regular", fontSize: 23, fontWeight: 600, color: colors.fg }, children: t2("settings.vibration") }), + /* @__PURE__ */ (0, jsx)(Text, { style: { fontFamily: "Inter-Regular", fontSize: 21, color: colors.fg }, children: levelName(t2, level) }) + ] }), + /* @__PURE__ */ (0, jsx)(Text, { style: { fontFamily: "Inter-Regular", fontSize: 21, color: colors.muted, lineHeight: 30 }, children: t2("settings.vibrationDetail") }) + ] }), + /* @__PURE__ */ (0, jsx)(View, { style: { flexDirection: "row", gap: 9 }, children: LEVELS.map((value) => /* @__PURE__ */ (0, jsx)(VibrationSegment, { level: value, chosen: value === level }, value)) }) + ] }); + } function SectionLabel({ icon, title, detail: detail2 }) { const text = /* @__PURE__ */ (0, jsxs)(View, { style: { flex: 1, gap: 3 }, children: [ /* @__PURE__ */ (0, jsx)(Text, { style: { fontFamily: "Inter-Regular", fontSize: 19, fontWeight: 600, color: colors.muted, letterSpacing: 1.5 }, children: title }), @@ -65,7 +89,6 @@ import { Text, View, useIsFocused } from '@ps5-react/core'; function LanguageSheet({ onClose }) { const t2 = useT(); const choice2 = languageChoice(); - const vibration = useNavigationHaptics(); const pick2 = (code) => { chooseLanguage(code); onClose(); @@ -76,7 +99,7 @@ import { Text, View, useIsFocused } from '@ps5-react/core'; /* @__PURE__ */ (0, jsx)(SectionLabel, { icon: "download", title: t2("settings.downloads") }), /* @__PURE__ */ (0, jsx)(SpeedLimit, {}), /* @__PURE__ */ (0, jsx)(SectionLabel, { title: t2("settings.controller") }), - /* @__PURE__ */ (0, jsx)(View, { focusKey: "settings:vibration", onPress: () => setNavigationHaptics(!vibration), style: (_state) => [{ position: "relative", flexDirection: "row", alignItems: "center", gap: 24, borderRadius: 12, paddingHorizontal: 18, paddingVertical: 18 }, _state.focused ? { backgroundColor: "#212125" } : null], children: /* @__PURE__ */ (0, jsx)(LanguageFace, { name: t2("settings.vibration"), detail: t2("settings.vibrationDetail"), checked: vibration }) }), + /* @__PURE__ */ (0, jsx)(VibrationLevel, {}), /* @__PURE__ */ (0, jsx)(SectionLabel, { icon: "languages", title: t2("language.title"), detail: t2("language.subtitle") }), /* @__PURE__ */ (0, jsx)(Row4, { code: "system", name: t2("language.system"), detail: nameOf(systemLocale()), checked: choice2 === null, onPress: () => pick2(null) }), LOCALES.map( diff --git a/app/ui/haptics.js b/app/ui/haptics.js index 5aad1f1..8220938 100644 --- a/app/ui/haptics.js +++ b/app/ui/haptics.js @@ -18,38 +18,45 @@ import { Controller, useNavigationEvents } from '@ps5-react/core'; // A pulse on every focus move, press and page change is a lot to hold for a whole browse, so // it starts off and lives in settings.json beside the language. The finished-download pulse // is a notice, not navigation, and stays on either way. + // Navigation pulses are scaled by a level in percent, 0 being off (issue #32). The setting + // used to be `vibration: true`, which reads as full strength. + var LEVELS = [0, 25, 50, 75, 100]; var SETTINGS = "settings.json"; - var navigationHaptics = loadList(SETTINGS, "settings").vibration === true; + var saved = loadList(SETTINGS, "settings").vibration; + var vibrationLevel = saved === true ? 100 : LEVELS.includes(saved) ? saved : 0; var listeners = /* @__PURE__ */ new Set(); - function setNavigationHaptics(on) { - navigationHaptics = on; + var navigationPulse = ([strength, ms]) => pulse2([strength * vibrationLevel / 100, ms]); + // A chosen level is felt at once, so the player can tell the steps apart while picking one. + function setVibrationLevel(level) { + vibrationLevel = level; const { vibration, ...settings } = loadList(SETTINGS, "settings"); - saveList(SETTINGS, "settings", on ? { ...settings, vibration: true } : settings); - for (const listener of listeners) listener(on); + saveList(SETTINGS, "settings", level ? { ...settings, vibration: level } : settings); + for (const listener of listeners) listener(level); + if (level) navigationPulse(PAGE); } - function useNavigationHaptics() { - const [on, setOn] = (0, useState)(navigationHaptics); + function useVibrationLevel() { + const [level, setLevel] = (0, useState)(vibrationLevel); (0, useEffect)(() => { - listeners.add(setOn); + listeners.add(setLevel); return () => { - listeners.delete(setOn); + listeners.delete(setLevel); }; }, []); - return on; + return level; } function useInterfaceHaptics() { useNavigationEvents((event) => { - if (!navigationHaptics) return; - if (event.type === "move") pulse2(TICK); + if (!vibrationLevel) return; + if (event.type === "move") navigationPulse(TICK); else if (event.type === "blocked") { - pulse2(EDGE); - setTimeout(() => pulse2(EDGE), EDGE_GAP_MS); - } else if (event.type === "press") pulse2(event.handled ? PRESS : EDGE); - else if (event.handled && event.type === "back") pulse2(BACK); - else if (event.handled && PAGE_ACTIONS.has(event.action)) pulse2(PAGE); - else if (event.handled) pulse2(PRESS); + navigationPulse(EDGE); + setTimeout(() => navigationPulse(EDGE), EDGE_GAP_MS); + } else if (event.type === "press") navigationPulse(event.handled ? PRESS : EDGE); + else if (event.handled && event.type === "back") navigationPulse(BACK); + else if (event.handled && PAGE_ACTIONS.has(event.action)) navigationPulse(PAGE); + else if (event.handled) navigationPulse(PRESS); }); } var pulseNotice = () => pulse2(NOTICE); -export { pulseNotice, setNavigationHaptics, useInterfaceHaptics, useNavigationHaptics }; +export { LEVELS, pulseNotice, setVibrationLevel, useInterfaceHaptics, useVibrationLevel };