├── .github ├── FUNDING.yml ├── workflows │ ├── build-container.yml │ ├── crowdin-contributors.yml │ └── release.yml └── ISSUE_TEMPLATE │ └── 01-incorrect-controller-color-detection.yml ├── src ├── mock-vue-router.ts ├── utils │ ├── func.util.ts │ ├── env.util.ts │ ├── decoder.util.ts │ ├── umami.util.ts │ ├── hid.util.ts │ ├── encoder.util.ts │ ├── labeled-value.util.ts │ ├── logger.util.ts │ ├── common.util.ts │ ├── lock.util.ts │ ├── color.util.ts │ ├── direction.util.ts │ ├── comp.util.ts │ ├── time.util.ts │ ├── pixi.util.ts │ ├── lang.util.ts │ ├── reactive.util.ts │ └── format.util.ts ├── lazy-motion.ts ├── global.d.ts ├── assets │ ├── dualshockPattern.webp │ ├── dualsenseEdgePattern.webp │ ├── base-components.scss │ ├── transitions.scss │ └── fonts.scss ├── composables │ ├── onDocumentUnload.ts │ ├── magicTeleportStore.ts │ ├── useOverlayHeader.ts │ ├── useInjectValues.ts │ ├── useToast │ │ └── ToastContainer.vue │ ├── useLangSpecFont.ts │ ├── useMagicTeleport.ts │ └── useEventBus.ts ├── components │ ├── common │ │ ├── TextTag.vue │ │ ├── LoadingView.vue │ │ ├── ControllerTextButton.vue │ │ ├── ContentTips.vue │ │ ├── ColorInput.vue │ │ ├── LabeledValue.vue │ │ ├── GeneralContainer.vue │ │ ├── VisualizerPanelShell.vue │ │ ├── GitVersion.vue │ │ ├── WidgetShell.vue │ │ ├── GroupedButton.vue │ │ ├── SwitchBox.vue │ │ ├── ConditionShell.vue │ │ ├── PWAPrompt.vue │ │ ├── HexPreview.vue │ │ └── HoldActiveButton.vue │ ├── Debug.vue │ ├── AccelValueBar.vue │ ├── DebugPanel.vue │ ├── base │ │ ├── DouButton.vue │ │ ├── DouSwitch.vue │ │ └── DouNumberInput.vue │ ├── OverlayHeader.vue │ ├── LangSwitcher.vue │ ├── GyroValueBar.vue │ ├── MainHeader.vue │ └── MainFooter.vue ├── router │ ├── DualSenseEdge │ │ ├── views │ │ │ ├── _Profile │ │ │ │ ├── pages │ │ │ │ │ └── CustomButtonMapping.vue │ │ │ │ ├── ProfileRenameInput.vue │ │ │ │ └── ProfileSwitchButton.vue │ │ │ ├── ModelPanel.vue │ │ │ ├── _ModelPanel │ │ │ │ └── DSETop.vue │ │ │ ├── HideInConfigModeLayout.vue │ │ │ └── _visualizerPanel │ │ │ │ ├── AccelView.vue │ │ │ │ └── GyroView.vue │ │ └── _utils │ │ │ ├── eventbus.util.ts │ │ │ ├── composable.util.ts │ │ │ └── offset.util.ts │ ├── DualSense │ │ ├── _utils │ │ │ ├── eventbus.util.ts │ │ │ └── offset.util.ts │ │ └── views │ │ │ ├── ModelPanel.vue │ │ │ └── _visualizerPanel │ │ │ ├── AccelView.vue │ │ │ └── GyroView.vue │ └── DualShockV2 │ │ ├── _utils │ │ ├── eventbus.util.ts │ │ └── offset.util.ts │ │ └── views │ │ ├── ModelPanel.vue │ │ ├── _OutputPanel │ │ └── outputStruct.ts │ │ └── _visualizerPanel │ │ ├── AccelView.vue │ │ └── GyroView.vue ├── @types │ └── global.d.ts ├── locales.ts ├── device-based-router │ ├── register-entry.ts │ └── index.ts ├── sw.ts ├── main.ts └── AppInner.vue ├── public ├── favicon.ico ├── pwa │ ├── mstile-70x70.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── mstile-144x144.png │ ├── mstile-150x150.png │ ├── mstile-310x150.png │ ├── mstile-310x310.png │ ├── apple-touch-icon.png │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ └── browserconfig.xml ├── fonts │ ├── sn-pro-var.woff2 │ ├── vazirmatn-arabic.woff2 │ ├── vazirmatn-latin.woff2 │ └── vazirmatn-latin-ext.woff2 └── .well-known │ └── manifest.webmanifest ├── design ├── dualsense.afdesign ├── dualshock.afdesign ├── all-models.afdesign ├── dualshockPattern.png ├── dualsenseEdge.afdesign ├── dualshockPattern.webp ├── dualsenseEdgePattern.png └── dualsenseEdgePattern.webp ├── crowdin.yml ├── .vscode └── extensions.json ├── dev ├── compose.yaml └── Dockerfile ├── tsconfig.json ├── plugins └── html-plugin │ ├── umami.html │ ├── ga4.html │ └── index.ts ├── packages └── fancy-controller │ ├── sets │ ├── Curve │ │ ├── curve-default.svg │ │ ├── curve-quick.svg │ │ ├── curve-precise.svg │ │ ├── curve-digital.svg │ │ ├── curve-dynamic.svg │ │ └── curve-steady.svg │ └── Button │ │ ├── l1-outline.svg │ │ ├── lt-outline.svg │ │ ├── square-outline.svg │ │ ├── triangle-outline.svg │ │ ├── y-outline.svg │ │ ├── a-outline.svg │ │ ├── x-outline.svg │ │ ├── l2-outline.svg │ │ ├── r1-outline.svg │ │ ├── rt-outline.svg │ │ ├── fn-outline.svg │ │ ├── square-solid.svg │ │ ├── l3-outline.svg │ │ ├── circle-outline.svg │ │ ├── lb-outline.svg │ │ ├── r2-outline.svg │ │ ├── circle-solid.svg │ │ ├── y-solid.svg │ │ ├── b-outline.svg │ │ ├── triangle-solid.svg │ │ ├── a-solid.svg │ │ ├── r3-outline.svg │ │ ├── rb-outline.svg │ │ ├── l1-twotone.svg │ │ ├── l1-twotone-bordered.svg │ │ ├── lt-twotone.svg │ │ ├── lt-twotone-bordered.svg │ │ ├── triangle-twotone.svg │ │ ├── x-twotone.svg │ │ ├── y-twotone.svg │ │ ├── square-twotone.svg │ │ ├── ls-outline.svg │ │ ├── a-twotone.svg │ │ ├── square-twotone-bordered.svg │ │ ├── b-solid.svg │ │ ├── l2-twotone.svg │ │ ├── r1-twotone.svg │ │ ├── triangle-twotone-bordered.svg │ │ ├── x-twotone-bordered.svg │ │ ├── y-twotone-bordered.svg │ │ ├── a-twotone-bordered.svg │ │ ├── l2-twotone-bordered.svg │ │ ├── r1-twotone-bordered.svg │ │ ├── x-solid.svg │ │ ├── rt-twotone.svg │ │ ├── fn-twotone.svg │ │ ├── rt-twotone-bordered.svg │ │ ├── lt-solid.svg │ │ ├── fn-twotone-bordered.svg │ │ ├── l3-twotone.svg │ │ ├── l1-solid.svg │ │ ├── l3-twotone-bordered.svg │ │ ├── circle-twotone.svg │ │ ├── lb-twotone.svg │ │ ├── r2-twotone.svg │ │ ├── lb-twotone-bordered.svg │ │ ├── b-twotone.svg │ │ ├── r2-twotone-bordered.svg │ │ ├── rs-outline.svg │ │ ├── circle-twotone-bordered.svg │ │ ├── r3-twotone.svg │ │ ├── b-twotone-bordered.svg │ │ ├── rb-twotone.svg │ │ ├── r3-twotone-bordered.svg │ │ ├── rb-twotone-bordered.svg │ │ ├── lb-solid.svg │ │ ├── ls-twotone.svg │ │ ├── ls-twotone-bordered.svg │ │ ├── rt-solid.svg │ │ ├── r1-solid.svg │ │ ├── rs-twotone.svg │ │ ├── fn-solid.svg │ │ ├── rs-twotone-bordered.svg │ │ ├── rb-solid.svg │ │ └── l2-solid.svg │ ├── package.json │ ├── README.md │ ├── tsconfig.json │ ├── LICENSE │ └── scripts │ └── build.ts ├── .editorconfig ├── .trae └── rules │ └── project_rules.md ├── tsconfig.node.json ├── .dockerignore ├── .gitignore ├── env.d.ts ├── tsconfig.app.json ├── eslint.config.mjs ├── prod └── Dockerfile ├── config ├── git.ts └── crowdin.ts ├── LICENSE ├── index.html ├── vercel.json └── uno.config.ts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | buy_me_a_coffee: daidr 2 | github: daidr 3 | -------------------------------------------------------------------------------- /src/mock-vue-router.ts: -------------------------------------------------------------------------------- 1 | export const useRoute = () => undefined 2 | -------------------------------------------------------------------------------- /src/utils/func.util.ts: -------------------------------------------------------------------------------- 1 | export function hole(..._args: unknown[]) { 2 | void _args 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daidr/dualsense-tester/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/lazy-motion.ts: -------------------------------------------------------------------------------- 1 | import { domAnimation } from 'motion-v' 2 | 3 | export default domAnimation 4 | -------------------------------------------------------------------------------- /design/dualsense.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daidr/dualsense-tester/HEAD/design/dualsense.afdesign -------------------------------------------------------------------------------- /design/dualshock.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daidr/dualsense-tester/HEAD/design/dualshock.afdesign -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | files: 2 | - source: /src/locales/en-US.json 3 | translation: /src/locales/%locale%.json 4 | -------------------------------------------------------------------------------- /design/all-models.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daidr/dualsense-tester/HEAD/design/all-models.afdesign -------------------------------------------------------------------------------- /design/dualshockPattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daidr/dualsense-tester/HEAD/design/dualshockPattern.png -------------------------------------------------------------------------------- /public/pwa/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daidr/dualsense-tester/HEAD/public/pwa/mstile-70x70.png -------------------------------------------------------------------------------- /src/utils/env.util.ts: -------------------------------------------------------------------------------- 1 | export const isDev = import.meta.env.DEV 2 | export const gitDefine = __GIT_DEFINE__ 3 | -------------------------------------------------------------------------------- /design/dualsenseEdge.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daidr/dualsense-tester/HEAD/design/dualsenseEdge.afdesign -------------------------------------------------------------------------------- /design/dualshockPattern.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daidr/dualsense-tester/HEAD/design/dualshockPattern.webp -------------------------------------------------------------------------------- /public/fonts/sn-pro-var.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daidr/dualsense-tester/HEAD/public/fonts/sn-pro-var.woff2 -------------------------------------------------------------------------------- /public/pwa/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daidr/dualsense-tester/HEAD/public/pwa/favicon-16x16.png -------------------------------------------------------------------------------- /public/pwa/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daidr/dualsense-tester/HEAD/public/pwa/favicon-32x32.png -------------------------------------------------------------------------------- /public/pwa/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daidr/dualsense-tester/HEAD/public/pwa/mstile-144x144.png -------------------------------------------------------------------------------- /public/pwa/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daidr/dualsense-tester/HEAD/public/pwa/mstile-150x150.png -------------------------------------------------------------------------------- /public/pwa/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daidr/dualsense-tester/HEAD/public/pwa/mstile-310x150.png -------------------------------------------------------------------------------- /public/pwa/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daidr/dualsense-tester/HEAD/public/pwa/mstile-310x310.png -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | import type { umami } from 'umami' 2 | 3 | declare global { 4 | const umami: umami.umami 5 | } 6 | -------------------------------------------------------------------------------- /design/dualsenseEdgePattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daidr/dualsense-tester/HEAD/design/dualsenseEdgePattern.png -------------------------------------------------------------------------------- /design/dualsenseEdgePattern.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daidr/dualsense-tester/HEAD/design/dualsenseEdgePattern.webp -------------------------------------------------------------------------------- /public/pwa/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daidr/dualsense-tester/HEAD/public/pwa/apple-touch-icon.png -------------------------------------------------------------------------------- /src/assets/dualshockPattern.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daidr/dualsense-tester/HEAD/src/assets/dualshockPattern.webp -------------------------------------------------------------------------------- /public/fonts/vazirmatn-arabic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daidr/dualsense-tester/HEAD/public/fonts/vazirmatn-arabic.woff2 -------------------------------------------------------------------------------- /public/fonts/vazirmatn-latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daidr/dualsense-tester/HEAD/public/fonts/vazirmatn-latin.woff2 -------------------------------------------------------------------------------- /public/pwa/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daidr/dualsense-tester/HEAD/public/pwa/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/pwa/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daidr/dualsense-tester/HEAD/public/pwa/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/assets/dualsenseEdgePattern.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daidr/dualsense-tester/HEAD/src/assets/dualsenseEdgePattern.webp -------------------------------------------------------------------------------- /public/fonts/vazirmatn-latin-ext.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daidr/dualsense-tester/HEAD/public/fonts/vazirmatn-latin-ext.woff2 -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "Vue.volar", 4 | "dbaeumer.vscode-eslint", 5 | "oxc.oxc-vscode", 6 | "antfu.unocss" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /dev/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | frontend: 3 | build: ./. 4 | container_name: frontend 5 | volumes: 6 | - ../:/app/ 7 | ports: 8 | - '5173:5173' 9 | -------------------------------------------------------------------------------- /src/utils/decoder.util.ts: -------------------------------------------------------------------------------- 1 | export const shiftJISDecoder = new TextDecoder('shift-jis') 2 | export const utf8Decoder = new TextDecoder('utf-8') 3 | export const utf16LEDecoder = new TextDecoder('utf-16le') 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { 4 | "path": "./tsconfig.node.json" 5 | }, 6 | { 7 | "path": "./tsconfig.app.json" 8 | } 9 | ], 10 | "files": [] 11 | } 12 | -------------------------------------------------------------------------------- /plugins/html-plugin/umami.html: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /dev/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM oven/bun:alpine 2 | 3 | RUN apk add git=2.45.3-r0 --no-cache 4 | 5 | WORKDIR /app 6 | 7 | RUN git config --global --add safe.directory /app 8 | 9 | CMD [ "bun", "run", "--bun", "dev" ] 10 | 11 | -------------------------------------------------------------------------------- /src/composables/onDocumentUnload.ts: -------------------------------------------------------------------------------- 1 | import { useEventListener } from '@vueuse/core' 2 | 3 | export function onDocumentUnload(fn: (e: BeforeUnloadEvent) => any) { 4 | useEventListener(window, 'beforeunload', (evt) => { 5 | return fn(evt) 6 | }) 7 | } 8 | -------------------------------------------------------------------------------- /src/components/common/TextTag.vue: -------------------------------------------------------------------------------- 1 | 3 | 4 | 9 | 10 | 13 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Curve/curve-default.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Curve/curve-quick.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /public/pwa/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #2f81f7 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/utils/umami.util.ts: -------------------------------------------------------------------------------- 1 | import { gitDefine } from './env.util' 2 | 3 | export function track(name: string, data?: Record) { 4 | if (!('umami' in window)) { 5 | return 6 | } 7 | window?.umami?.track(name, { 8 | version: gitDefine.shortCommitHash, 9 | ...data || {}, 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /src/router/DualSenseEdge/views/_Profile/pages/CustomButtonMapping.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/composables/magicTeleportStore.ts: -------------------------------------------------------------------------------- 1 | import type { ShallowReactive, ShallowRef, VNode } from 'vue' 2 | 3 | interface MagicTeleportItem { 4 | refCount: number 5 | vNode: VNode | VNode[] | null 6 | triggerRef: ShallowRef 7 | } 8 | 9 | export const _magicTeleportStore = new Map>() 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | insert_final_newline = true 7 | quote_type = auto 8 | space_after_anonymous_functions = true 9 | space_after_control_statements = true 10 | spaces_around_operators = true 11 | trim_trailing_whitespace = true 12 | spaces_in_brackets = false 13 | end_of_line = lf 14 | -------------------------------------------------------------------------------- /src/@types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Document { 3 | startViewTransition?: (callback: () => Promise | void) => { 4 | finished: Promise 5 | updateCallbackDone: Promise 6 | ready: Promise 7 | } 8 | } 9 | interface Window { 10 | debug: any 11 | } 12 | } 13 | 14 | export {} 15 | -------------------------------------------------------------------------------- /src/utils/hid.util.ts: -------------------------------------------------------------------------------- 1 | import { hidLogger } from './logger.util' 2 | 3 | export async function requestHIDDevice(filters: HIDDeviceFilter[]): Promise { 4 | try { 5 | await navigator.hid.requestDevice({ 6 | filters, 7 | }) 8 | } 9 | catch (error) { 10 | hidLogger.error(error) 11 | return false 12 | } 13 | return true 14 | } 15 | -------------------------------------------------------------------------------- /src/components/Debug.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | 15 | 18 | -------------------------------------------------------------------------------- /.trae/rules/project_rules.md: -------------------------------------------------------------------------------- 1 | 1. When generating commit messages, please follow the Conventional Commits specification. Commit messages must be written in English only. 2 | 2. The project uses Vue 3 framework, TypeScript as the primary programming language, and Vite as the build tool. 3 | 3. The project uses UnoCSS atomic CSS. 4 | 4. Please use bun as the package manager and runtime. 5 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Curve/curve-precise.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/utils/encoder.util.ts: -------------------------------------------------------------------------------- 1 | export const utf16LEEncoder = { 2 | encode: (string: string) => { 3 | const utf16leBytes = new Uint8Array(string.length * 2) 4 | for (let i = 0; i < string.length; i++) { 5 | const codeUnit = string.charCodeAt(i) 6 | utf16leBytes[i * 2] = codeUnit & 0xFF 7 | utf16leBytes[i * 2 + 1] = (codeUnit >> 8) & 0xFF 8 | } 9 | return utf16leBytes 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /src/router/DualSense/_utils/eventbus.util.ts: -------------------------------------------------------------------------------- 1 | import { useEventBus } from '@/composables/useEventBus' 2 | 3 | export const [useEventBusRegister, useEventBusEmit] = useEventBus<{ 4 | 'output:set-speaker-volume': [number] 5 | 'output:set-headphone-volume': [number] 6 | 'output:store-speaker-volume': [] 7 | 'output:store-headphone-volume': [] 8 | 'output:retrieve-speaker-volume': [] 9 | 'output:retrieve-headphone-volume': [] 10 | }>() 11 | -------------------------------------------------------------------------------- /src/router/DualShockV2/_utils/eventbus.util.ts: -------------------------------------------------------------------------------- 1 | import { useEventBus } from '@/composables/useEventBus' 2 | 3 | export const [useEventBusRegister, useEventBusEmit] = useEventBus<{ 4 | 'output:set-speaker-volume': [number] 5 | 'output:set-headphone-volume': [number] 6 | 'output:store-speaker-volume': [] 7 | 'output:store-headphone-volume': [] 8 | 'output:retrieve-speaker-volume': [] 9 | 'output:retrieve-headphone-volume': [] 10 | }>() 11 | -------------------------------------------------------------------------------- /src/router/DualSenseEdge/_utils/eventbus.util.ts: -------------------------------------------------------------------------------- 1 | import { useEventBus } from '@/composables/useEventBus' 2 | 3 | export const [useEventBusRegister, useEventBusEmit] = useEventBus<{ 4 | 'output:set-speaker-volume': [number] 5 | 'output:set-headphone-volume': [number] 6 | 'output:store-speaker-volume': [] 7 | 'output:store-headphone-volume': [] 8 | 'output:retrieve-speaker-volume': [] 9 | 'output:retrieve-headphone-volume': [] 10 | }>() 11 | -------------------------------------------------------------------------------- /src/components/common/LoadingView.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/.well-known/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"DualSense Tester","short_name":"DualSense Tester","display_override":["window-controls-overlay"],"display":"standalone","scope":"/","start_url":"/","version":"0.0.1","icons":[{"src":"/pwa/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/pwa/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"permissions_policy":{"hid":["self"],"direct-sockets":["self"],"cross-origin-isolated":["self"]}} -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/bun/tsconfig.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Bundler", 7 | "skipLibCheck": true 8 | }, 9 | "include": [ 10 | "./**/vite.config.*", 11 | "./**/vitest.config.*", 12 | "./**/cypress.config.*", 13 | "./**/nightwatch.conf.*", 14 | "./**/playwright.config.*", 15 | "./config/**/*", 16 | "./plugins/**/*" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/labeled-value.util.ts: -------------------------------------------------------------------------------- 1 | import type { VNode } from 'vue' 2 | 3 | export function createLabeledValueItem(label: string, value: string | Promise, valueLocalePrefix?: string, tooltip?: VNode): LabeledValueItem { 4 | return { 5 | label, 6 | value, 7 | valueLocalePrefix, 8 | tooltip, 9 | } 10 | } 11 | 12 | export interface LabeledValueItem { 13 | label: string 14 | value: string | Promise 15 | valueLocalePrefix?: string 16 | tooltip?: VNode 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/logger.util.ts: -------------------------------------------------------------------------------- 1 | import { createConsola, LogLevels } from 'consola' 2 | import { isDev } from './env.util' 3 | 4 | export const consola = createConsola({ 5 | level: isDev ? LogLevels.verbose : LogLevels.info, 6 | }) 7 | 8 | export const routerLogger = consola.withTag('ROUTER') 9 | export const hidLogger = consola.withTag('HID') 10 | export const uiLogger = consola.withTag('UI') 11 | export const lockLogger = consola.withTag('LOCK') 12 | export const eventBusLogger = consola.withTag('EVENTBUS') 13 | -------------------------------------------------------------------------------- /src/composables/useOverlayHeader.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | 3 | export function useOverlayHeader() { 4 | const showOverlayHeader = ref(false) 5 | if ('windowControlsOverlay' in navigator) { 6 | showOverlayHeader.value = (navigator.windowControlsOverlay as any).visible; 7 | (navigator.windowControlsOverlay as any).addEventListener( 8 | 'geometrychange', 9 | (event: any) => { 10 | showOverlayHeader.value = event.visible 11 | }, 12 | ) 13 | } 14 | return showOverlayHeader 15 | } 16 | -------------------------------------------------------------------------------- /src/locales.ts: -------------------------------------------------------------------------------- 1 | const LOCALES: Record = { 2 | 'zh-CN': '简体中文', 3 | 'en-US': 'English (US)', 4 | 'ru-RU': 'Русский', 5 | 'fa-IR': 'فارسی', 6 | 'ar-EG': 'العربية (مصر)', 7 | 'ar-SA': 'العربية (السعودية)', 8 | 'it-IT': 'Italiano (Italia)', 9 | 'uk-UA': 'Українська', 10 | 'el-GR': 'Ελληνικά', 11 | 'tr-TR': 'Türkçe', 12 | 'pt-BR': 'Português (BR)', 13 | 'de-DE': 'Deutsch', 14 | } 15 | 16 | export function getLocaleLabel(locale: string): string { 17 | return LOCALES[locale] || 'Unknown' 18 | } 19 | -------------------------------------------------------------------------------- /packages/fancy-controller/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fancy-controller", 3 | "type": "module", 4 | "private": true, 5 | "author": { 6 | "name": "Xuezhou Dai", 7 | "url": "https://github.com/daidr" 8 | }, 9 | "license": "MIT", 10 | "scripts": { 11 | "build": "bun run ./scripts/build.ts" 12 | }, 13 | "peerDependencies": { 14 | "typescript": "^5.9.3" 15 | }, 16 | "dependencies": { 17 | "@iconify/tools": "^4.1.4" 18 | }, 19 | "devDependencies": { 20 | "@types/bun": "catalog:" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/common.util.ts: -------------------------------------------------------------------------------- 1 | export function isObjectShallowEqual(obj1: Record | undefined, obj2: Record | undefined) { 2 | if (!obj1 || !obj2) { 3 | return false 4 | } 5 | const keys1 = Object.keys(obj1) 6 | const keys2 = Object.keys(obj2) 7 | return keys1.length === keys2.length && keys1.every(key => obj1[key] === obj2[key]) 8 | } 9 | 10 | export const shellVariants = { 11 | visible: { opacity: 1, transition: { when: 'beforeChildren' } }, 12 | hidden: { opacity: 0, transition: { when: 'afterChildren' } }, 13 | } 14 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/l1-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/components/common/ControllerTextButton.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 19 | 20 | 22 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist_iwa 14 | dist-ssr 15 | coverage 16 | *.local 17 | 18 | /cypress/videos/ 19 | /cypress/screenshots/ 20 | 21 | # Editor directories and files 22 | .vscode/* 23 | !.vscode/extensions.json 24 | !.vscode/settings.json 25 | .idea 26 | *.suo 27 | *.ntvs* 28 | *.njsproj 29 | *.sln 30 | *.sw? 31 | 32 | /private/* 33 | 34 | .env.* 35 | .env 36 | .vercel 37 | .env*.local 38 | 39 | /api 40 | /design 41 | -------------------------------------------------------------------------------- /src/utils/lock.util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * const lock = createAsyncLock(); 3 | * 4 | * return await lock(async () => { 5 | * return await someAsyncFunction(); 6 | * }); 7 | */ 8 | export function createAsyncLock() { 9 | let lock = Promise.resolve() 10 | return async function (fn: () => Promise): Promise { 11 | const _lock = lock 12 | let release = () => {} 13 | lock = new Promise((resolve) => { 14 | release = resolve 15 | }) 16 | try { 17 | return await _lock.then(fn) 18 | } 19 | finally { 20 | release() 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist_iwa 14 | dist-ssr 15 | coverage 16 | *.local 17 | 18 | /cypress/videos/ 19 | /cypress/screenshots/ 20 | 21 | # Editor directories and files 22 | .vscode/* 23 | !.vscode/extensions.json 24 | !.vscode/settings.json 25 | .idea 26 | *.suo 27 | *.ntvs* 28 | *.njsproj 29 | *.sln 30 | *.sw? 31 | 32 | /private/* 33 | 34 | .env.* 35 | .env 36 | .vercel 37 | .env*.local 38 | 39 | # Backup files 40 | *.bak 41 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | declare interface GitInfo { 6 | owner: string 7 | repo: string 8 | branch: string 9 | pr: string 10 | commitHash: string 11 | shortCommitHash: string 12 | commitMessage: string 13 | commitTimestamp: string 14 | } 15 | 16 | declare const __CROWDIN_PROGRESS__: Record 21 | 22 | declare const __GIT_DEFINE__: GitInfo 23 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/lt-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /plugins/html-plugin/ga4.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/utils/color.util.ts: -------------------------------------------------------------------------------- 1 | export function hexToRgb(hex: string): [number, number, number] { 2 | const hexCode = hex.replace(/^#/, '') 3 | const r = Number.parseInt(hexCode.substring(0, 2), 16) 4 | const g = Number.parseInt(hexCode.substring(2, 4), 16) 5 | const b = Number.parseInt(hexCode.substring(4, 6), 16) 6 | return [r, g, b] 7 | } 8 | 9 | export function rgbToHex(rgb: [number, number, number]): string { 10 | const [r, g, b] = rgb 11 | return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b 12 | .toString(16) 13 | .padStart(2, '0')}` 14 | } 15 | -------------------------------------------------------------------------------- /src/components/common/ContentTips.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/router/DualSenseEdge/_utils/composable.util.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue' 2 | import { useConnectionType, useInputReport } from '@/composables/useInjectValues' 3 | import { DeviceConnectionType } from '@/device-based-router/shared' 4 | 5 | export function useInNormalMode() { 6 | const inputReport = useInputReport() 7 | const connectionType = useConnectionType() 8 | const inNormalMode = computed(() => { 9 | const byte = inputReport.value?.getInt8(connectionType.value === DeviceConnectionType.USB ? 48 : 49) 10 | return byte && (byte & 0b00000011) === 0 11 | }) 12 | return inNormalMode 13 | } 14 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/square-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/triangle-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/y-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/a-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/x-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/utils/direction.util.ts: -------------------------------------------------------------------------------- 1 | const rtlCodeList = [ 2 | 'ar', 3 | 'ar-AE', 4 | 'ar-BH', 5 | 'ar-DJ', 6 | 'ar-DZ', 7 | 'ar-EG', 8 | 'ar-IQ', 9 | 'ar-JO', 10 | 'ar-KW', 11 | 'ar-LB', 12 | 'ar-LY', 13 | 'ar-MA', 14 | 'ar-OM', 15 | 'ar-QA', 16 | 'ar-SA', 17 | 'ar-SD', 18 | 'ar-SY', 19 | 'ar-TN', 20 | 'ar-YE', 21 | 'fa-AF', 22 | 'fa-IR', 23 | 'he', 24 | 'he-IL', 25 | 'iw', 26 | 'kd', 27 | 'pk-PK', 28 | 'ps', 29 | 'ug', 30 | 'ur', 31 | 'ur-IN', 32 | 'ur-PK', 33 | 'yi', 34 | 'yi-US', 35 | ] 36 | 37 | export function checkRTL(localeCode: string) { 38 | return rtlCodeList.includes(localeCode) 39 | } 40 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "lib": [ 6 | "ES2020", 7 | "DOM", 8 | "WebWorker", 9 | "DOM.Iterable" 10 | ], 11 | "baseUrl": ".", 12 | "paths": { 13 | "@/*": [ 14 | "src/*" 15 | ] 16 | }, 17 | "typeRoots": [ 18 | "src/@types" 19 | ], 20 | "verbatimModuleSyntax": true 21 | }, 22 | "include": [ 23 | "env.d.ts", 24 | "src/**/*", 25 | "src/**/*.ts", 26 | "src/**/*.json", 27 | "src/**/*.vue" 28 | ], 29 | "exclude": [ 30 | "src/**/__tests__/*" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /src/components/AccelValueBar.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | 21 | 26 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/l2-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/r1-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/components/DebugPanel.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/common/ColorInput.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | import oxlint from 'eslint-plugin-oxlint' 3 | 4 | export default antfu( 5 | { 6 | unocss: true, 7 | rules: { 8 | '@unocss/order': 'off', 9 | 'no-console': ['warn', { allow: ['warn', 'error'] }], 10 | 'unused-imports/no-unused-vars': ['warn', { 11 | caughtErrors: 'none', 12 | argsIgnorePattern: '^_', 13 | }], 14 | 'no-lone-blocks': 'off', 15 | 'vue/no-mutating-props': ['error', { 16 | shallowOnly: true, 17 | }], 18 | }, 19 | }, 20 | { 21 | ignores: ['*.json', 'dist/**/*', 'dist-type/**/*'], 22 | }, 23 | ...oxlint.buildFromOxlintConfigFile('./.oxlintrc.json'), 24 | ) 25 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/rt-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/base-components.scss: -------------------------------------------------------------------------------- 1 | .dou { 2 | &-icon-link { 3 | @apply w-7 h-7 text-lg; 4 | @apply flex justify-center items-center; 5 | @apply text-primary rounded-full; 6 | @apply transition-colors; 7 | @apply relative; 8 | 9 | &::before { 10 | @apply content-empty absolute top-0 left-0 right-0 bottom-0; 11 | @apply border-1.5 border-primary/0 rounded-full; 12 | @apply transition transform-gpu; 13 | } 14 | 15 | &:hover { 16 | @apply text-white bg-primary; 17 | 18 | &::before { 19 | @apply scale-130 border-primary/30 dark-border-primary/50; 20 | } 21 | } 22 | 23 | &:active::before { 24 | @apply scale-100; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/components/common/LabeledValue.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | 22 | 30 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/fn-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Curve/curve-digital.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/square-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/fancy-controller/README.md: -------------------------------------------------------------------------------- 1 | # fancy-controller 2 | 3 | A icon set featuring game controller elements and buttons. This collection provides clean, consistent graphics for implementing controller-related UI in your website. 4 | 5 | [Open in Figma](https://www.figma.com/design/VTFbjb0DoOXOCODprXhHeo/Controller-Icon-Sets?node-id=0-1&t=cny2LNuGezyAq55N-1) 6 | 7 | ## License 8 | 9 | This icon set is licensed under the [MIT License](./LICENSE). 10 | 11 | **Disclaimer:** This project is not affiliated with, endorsed by, or connected to Sony PlayStation, Xbox, or any other gaming platform. Before using icons representing trademarks or logos of these companies, please consult their respective trademark guidelines and official documentation. 12 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/l3-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/composables/useInjectValues.ts: -------------------------------------------------------------------------------- 1 | import type { ShallowRef } from 'vue' 2 | import type { DeviceItemWithRouter } from '@/device-based-router/shared' 3 | import { computed, inject } from 'vue' 4 | 5 | export function useInputReport() { 6 | const inputReport = inject>('inputReport')! 7 | return inputReport 8 | } 9 | 10 | export function useInputReportId() { 11 | const inputReportId = inject>('inputReportId')! 12 | return inputReportId 13 | } 14 | 15 | export function useDevice() { 16 | const device = inject>('deviceItem')! 17 | return device 18 | } 19 | 20 | export function useConnectionType() { 21 | const device = useDevice() 22 | return computed(() => device.value.connectionType) 23 | } 24 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/circle-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/device-based-router/register-entry.ts: -------------------------------------------------------------------------------- 1 | import type { RouterManager } from '.' 2 | import { routerLogger } from '@/utils/logger.util' 3 | import { BaseDeviceRouter } from './shared' 4 | 5 | export function registerRouters(routerManager: RouterManager) { 6 | // 收集所有定义的路由 7 | const routerDefinitions = import.meta.glob('@/router/*/index.ts', { eager: true, import: 'default' }) 8 | for (const [defPath, defModule] of Object.entries(routerDefinitions)) { 9 | const RouterClass = defModule as any 10 | if (RouterClass.prototype instanceof BaseDeviceRouter) { 11 | routerLogger.debug('Registering router:', defPath) 12 | routerManager.register(new RouterClass()) 13 | } 14 | else { 15 | routerLogger.warn('Skipping non-router module:', defPath) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/comp.util.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeRefOrGetter, VNode } from 'vue' 2 | import { defineComponent, h, isVNode, toValue } from 'vue' 3 | 4 | export const RenderComponent = defineComponent((props: { node: MaybeRefOrGetter, className?: string, textClassName?: string }) => { 5 | return () => { 6 | if (isVNode(props.node)) { 7 | return props.node 8 | } 9 | else { 10 | const result = toValue(props.node) 11 | if (typeof result === 'string') { 12 | return h('span', { 13 | class: props.textClassName, 14 | }, result) 15 | } 16 | else { 17 | return h(result, { 18 | class: props.className, 19 | }) 20 | } 21 | } 22 | } 23 | }, { 24 | props: ['node', 'className', 'textClassName'], 25 | }) 26 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/lb-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/r2-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/circle-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/y-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/b-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Curve/curve-dynamic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Curve/curve-steady.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/sw.ts: -------------------------------------------------------------------------------- 1 | import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching' 2 | import { NavigationRoute, registerRoute } from 'workbox-routing' 3 | import { gitDefine } from '@/utils/env.util' 4 | 5 | declare let self: ServiceWorkerGlobalScope 6 | 7 | self.addEventListener('message', (event) => { 8 | if (!event.data) { 9 | return 10 | } 11 | if (event.data.type === 'SKIP_WAITING') { 12 | self.skipWaiting() 13 | } 14 | else if (event.data.type === 'GET_GIT_VERSION_INFO') { 15 | event.ports[0].postMessage({ 16 | type: 'GET_GIT_VERSION_INFO_RESP', 17 | gitDefine, 18 | }) 19 | } 20 | }) 21 | 22 | cleanupOutdatedCaches() 23 | 24 | precacheAndRoute(self.__WB_MANIFEST) 25 | 26 | registerRoute( 27 | new NavigationRoute(createHandlerBoundToURL('index.html'), {}), 28 | ) 29 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/triangle-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/utils/time.util.ts: -------------------------------------------------------------------------------- 1 | export function formatFirmwareTime(firmwareTime: string): string { 2 | return `${firmwareTime.slice(0, -8)} ${firmwareTime.slice(-8)}` 3 | } 4 | 5 | export async function sleep(ms: number) { 6 | return new Promise(resolve => setTimeout(resolve, ms)) 7 | } 8 | 9 | export function timestampToLEBuffer(timestamp: number): ArrayBuffer { 10 | const buffer = new ArrayBuffer(6) 11 | const view = new DataView(buffer) 12 | view.setUint32(0, timestamp & 0xFFFFFFFF, true) 13 | view.setUint16(4, Math.floor(timestamp / 0x100000000), true) 14 | return buffer 15 | } 16 | 17 | export function leBufferToTimestamp(buffer: ArrayBuffer): number { 18 | const view = new DataView(buffer) 19 | let timestamp = 0 20 | timestamp += view.getUint32(0, true) 21 | timestamp += view.getUint16(4, true) * 0x100000000 22 | return timestamp 23 | } 24 | -------------------------------------------------------------------------------- /packages/fancy-controller/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "jsx": "react-jsx", 5 | // Environment setup & latest features 6 | "lib": ["ESNext"], 7 | "moduleDetection": "force", 8 | "module": "Preserve", 9 | 10 | // Bundler mode 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "allowJs": true, 14 | 15 | // Best practices 16 | "strict": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "noImplicitOverride": true, 19 | 20 | "noPropertyAccessFromIndexSignature": false, 21 | "noUncheckedIndexedAccess": true, 22 | // Some stricter flags (disabled by default) 23 | "noUnusedLocals": false, 24 | "noUnusedParameters": false, 25 | "noEmit": true, 26 | "verbatimModuleSyntax": true, 27 | "skipLibCheck": true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/a-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/r3-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/transitions.scss: -------------------------------------------------------------------------------- 1 | .blur-fade-enter-active { 2 | transition: 3 | opacity 0.15s ease-out, 4 | filter 0.15s ease-out, 5 | transform 0.15s ease-out; 6 | } 7 | 8 | .blur-fade-leave-active { 9 | transition: 10 | opacity 0.15s ease-in, 11 | filter 0.15s ease-in, 12 | transform 0.15s ease-in; 13 | } 14 | 15 | .blur-fade-enter-from, 16 | .blur-fade-leave-to { 17 | opacity: 0; 18 | filter: blur(20px); 19 | transform: translateY(10px); 20 | } 21 | 22 | .blur-fade-enter-to, 23 | .blur-fade-leave-from { 24 | opacity: 1; 25 | filter: blur(0); 26 | transform: translateY(0); 27 | } 28 | 29 | .fade-enter-active, 30 | .fade-leave-active { 31 | transition: opacity 0.15s linear; 32 | } 33 | 34 | .fade-enter-from, 35 | .fade-leave-to { 36 | opacity: 0; 37 | } 38 | 39 | .blur-fade-enter-to, 40 | .blur-fade-leave-from { 41 | opacity: 1; 42 | } 43 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/rb-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /prod/Dockerfile: -------------------------------------------------------------------------------- 1 | # build environment 2 | FROM oven/bun:1 AS base 3 | WORKDIR /app 4 | RUN apt-get update && \ 5 | apt-get install --no-install-recommends -y git=1:2.39.5-0+deb12u2 && \ 6 | rm -rf /var/lib/apt/lists/* 7 | 8 | # Install dependencies 9 | FROM base AS install 10 | RUN mkdir -p /temp/dev 11 | COPY package.json bun.lock /temp/dev/ 12 | COPY packages/fancy-controller/package.json /temp/dev/packages/fancy-controller/ 13 | WORKDIR /temp/dev 14 | RUN bun install --frozen-lockfile 15 | 16 | # build stage 17 | FROM base AS build 18 | COPY --from=install /temp/dev/node_modules node_modules 19 | COPY . . 20 | ENV NODE_ENV=production 21 | ENV BUILD_ENV=docker 22 | RUN bun run --bun build-only 23 | 24 | # production stage 25 | FROM nginx:alpine3.21-slim AS production 26 | COPY --from=build /app/dist /usr/share/nginx/html 27 | EXPOSE 80 28 | CMD ["nginx", "-g", "daemon off;"] 29 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/l1-twotone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/l1-twotone-bordered.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/lt-twotone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/lt-twotone-bordered.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /config/git.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import { simpleGit } from 'simple-git' 3 | 4 | export async function gitDefine() { 5 | const git = simpleGit() 6 | const owner = process.env.VERCEL_GIT_REPO_OWNER || 'daidr' 7 | const repo = process.env.VERCEL_GIT_REPO_SLUG || 'dualsense-tester' 8 | const branch = process.env.VERCEL_GIT_COMMIT_REF || 'main' 9 | const pr = process.env.VERCEL_GIT_PULL_REQUEST_ID || '' 10 | const commit = (await git.log({ n: 1 })).latest 11 | const commitHash = commit?.hash || '' 12 | const shortCommitHash = commit?.hash?.slice(0, 7) || '' 13 | const commitMessage = commit?.message || '' 14 | const commitTimestamp = commit?.date || '' 15 | 16 | return { 17 | __GIT_DEFINE__: JSON.stringify({ 18 | owner, 19 | repo, 20 | branch, 21 | pr, 22 | commitHash, 23 | shortCommitHash, 24 | commitMessage, 25 | commitTimestamp, 26 | }), 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/triangle-twotone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/x-twotone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/y-twotone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/router/DualSense/views/ModelPanel.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 26 | 27 | 36 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/square-twotone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/components/base/DouButton.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 14 | 40 | -------------------------------------------------------------------------------- /src/router/DualShockV2/views/ModelPanel.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 26 | 27 | 36 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/ls-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/a-twotone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/square-twotone-bordered.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/b-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/l2-twotone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/r1-twotone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/triangle-twotone-bordered.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/x-twotone-bordered.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/y-twotone-bordered.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/a-twotone-bordered.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/l2-twotone-bordered.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/r1-twotone-bordered.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/router/DualShockV2/views/_OutputPanel/outputStruct.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | 3 | export class OutputStruct { 4 | #length = 73 5 | 6 | private sort = [ 7 | 'hwControl', 8 | 'audioControl', 9 | 'validFlag0', 10 | 'validFlag1', 11 | 'reserved', 12 | 'motorRight', 13 | 'motorLeft', 14 | 'ledRed', 15 | 'ledGreen', 16 | 'ledBlue', 17 | 'ledBlinkOn', 18 | 'ledBlinkOff', 19 | ] as const 20 | 21 | hwControl = ref(0xC4) 22 | audioControl = ref(0) 23 | validFlag0 = ref(0) 24 | validFlag1 = ref(0) 25 | reserved = ref(0) 26 | motorRight = ref(0) 27 | motorLeft = ref(0) 28 | ledRed = ref(0) 29 | ledGreen = ref(0) 30 | ledBlue = ref(0) 31 | ledBlinkOn = ref(0) 32 | ledBlinkOff = ref(0) 33 | 34 | get reportData() { 35 | const usedLength = this.sort.length 36 | const data = new Uint8Array(this.#length) 37 | for (let i = 0; i < usedLength; i++) { 38 | data[i] = this[this.sort[i]].value 39 | } 40 | return data 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/x-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/rt-twotone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/fn-twotone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/rt-twotone-bordered.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/router/DualShockV2/_utils/offset.util.ts: -------------------------------------------------------------------------------- 1 | export const inputReportOffsetUSB = createInputReportOffset(true) 2 | export const inputReportOffsetBluetooth = createInputReportOffset(false) 3 | 4 | export function createInputReportOffset(usb: boolean) { 5 | const num = usb ? 0 : 2 6 | const offset = { 7 | analogStickLX: 0 + num, 8 | analogStickLY: 1 + num, 9 | analogStickRX: 2 + num, 10 | analogStickRY: 3 + num, 11 | digitalKeys: 4 + num, // 3 12 | sequenceNum: 6 + num, // 1 13 | analogTriggerL: 7 + num, 14 | analogTriggerR: 8 + num, 15 | motionTimeStamp: 9 + num, 16 | motionTemperature: 11 + num, 17 | gyroPitch: 12 + num, 18 | gyroYaw: 14 + num, 19 | gyroRoll: 16 + num, 20 | accelX: 18 + num, 21 | accelY: 20 + num, 22 | accelZ: 22 + num, 23 | reserved2: 24 + num, 24 | status: 29 + num, 25 | reserved3: 31 + num, 26 | touchData: 34 + num, 27 | seqTag: 0, 28 | crc32: 0, 29 | } 30 | 31 | if (!usb) { 32 | offset.crc32 = 73 33 | } 34 | 35 | return offset 36 | } 37 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/lt-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/fn-twotone-bordered.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/components/common/GeneralContainer.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 27 | 28 | 39 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/l3-twotone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/l1-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/l3-twotone-bordered.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/circle-twotone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/utils/pixi.util.ts: -------------------------------------------------------------------------------- 1 | import { Application } from 'pixi.js' 2 | import { onBeforeUnmount, ref } from 'vue' 3 | 4 | export function createPixiApplication() { 5 | const app = new Application() 6 | return app 7 | } 8 | 9 | export async function usePixiApp(canvas: HTMLCanvasElement | null) { 10 | if (!canvas) { 11 | throw new Error('Canvas reference is required to create a Pixi Application.') 12 | } 13 | 14 | const isDisposed = ref(false) 15 | let initedResolver = () => { } 16 | const inited = new Promise((resolve) => { 17 | initedResolver = resolve 18 | }) 19 | 20 | const app = createPixiApplication() 21 | onBeforeUnmount(() => { 22 | isDisposed.value = true 23 | inited.then(() => { 24 | app.destroy({ 25 | removeView: true, 26 | }) 27 | }) 28 | }) 29 | await app.init({ 30 | preference: 'webgl', 31 | canvas, 32 | resizeTo: canvas, 33 | backgroundAlpha: 0, 34 | antialias: true, 35 | resolution: window.devicePixelRatio, 36 | }) 37 | initedResolver() 38 | return { 39 | app, 40 | isDisposed, 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/lb-twotone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/r2-twotone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/lb-twotone-bordered.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import messages from '@intlify/unplugin-vue-i18n/messages' 2 | import FloatingVue from 'floating-vue' 3 | import { createPinia } from 'pinia' 4 | 5 | import { createApp } from 'vue' 6 | import { createI18n } from 'vue-i18n' 7 | import { getAvailableLanguages, getPreferredLanguage } from '@/utils/lang.util' 8 | 9 | import App from './App.vue' 10 | import './assets/main.scss' 11 | import 'virtual:uno.css' 12 | import '@unocss/reset/tailwind.css' 13 | 14 | const app = createApp(App) 15 | 16 | // #region Pinia 17 | const pinia = createPinia() 18 | app.use(pinia) 19 | // #endregion 20 | 21 | app.use(FloatingVue) 22 | 23 | // #region I18n 24 | let lang = localStorage.getItem('lang') || navigator.language 25 | if (!getAvailableLanguages().includes(lang)) { 26 | lang = getPreferredLanguage(navigator.languages) 27 | localStorage.setItem('lang', lang) 28 | } 29 | 30 | export const i18n = createI18n({ 31 | locale: lang, 32 | fallbackLocale: 'en-US', 33 | warnHtmlMessage: false, 34 | messages, 35 | legacy: false, 36 | globalInjection: true, 37 | }) 38 | 39 | app.use(i18n) 40 | // #endregion 41 | 42 | app.mount('#app') 43 | -------------------------------------------------------------------------------- /src/router/DualSenseEdge/views/ModelPanel.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 30 | 31 | 39 | -------------------------------------------------------------------------------- /.github/workflows/build-container.yml: -------------------------------------------------------------------------------- 1 | name: Build Production Docker Image 2 | 3 | on: 4 | push: 5 | branches: 6 | - main # Trigger workflow on push to the main branch 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build-and-push: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | # Checkout the repository 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | 18 | # Log in to GitHub Container Registry 19 | - name: Log in to GitHub Container Registry 20 | uses: docker/login-action@v2 21 | with: 22 | registry: ghcr.io 23 | username: ${{ github.actor }} 24 | password: ${{ secrets.GITHUB_TOKEN }} # GitHub's token to authenticate 25 | 26 | # Build and tag the Docker image for GHCR 27 | - name: Build Docker image 28 | run: | 29 | docker build -f prod/Dockerfile -t ghcr.io/${{ github.repository_owner }}/dualsense-tester:latest . 30 | 31 | # Push the Docker image to GHCR 32 | - name: Push Docker image 33 | run: | 34 | docker push ghcr.io/${{ github.repository_owner }}/dualsense-tester:latest 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Xuezhou Dai (daidr) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/b-twotone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/r2-twotone-bordered.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/rs-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/circle-twotone-bordered.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/router/DualSenseEdge/views/_ModelPanel/DSETop.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /packages/fancy-controller/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Xuezhou Dai (daidr) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/01-incorrect-controller-color-detection.yml: -------------------------------------------------------------------------------- 1 | name: Incorrect Controller Color Detection 2 | description: Report an issue with the color detection of your controller. 3 | title: '[Bug] Incorrect Color Detection' 4 | labels: [bug, controller color] 5 | assignees: 6 | - daidr 7 | body: 8 | - type: dropdown 9 | id: model 10 | attributes: 11 | label: Model 12 | description: What model of controller are you using? 13 | options: 14 | - DualSense 15 | - DualSense Edge 16 | validations: 17 | required: true 18 | - type: input 19 | id: serial-number 20 | attributes: 21 | label: Serial Number 22 | description: What is the serial number of your controller? 23 | validations: 24 | required: true 25 | - type: input 26 | id: color 27 | attributes: 28 | label: Actual Color 29 | description: What is the actual color of your controller? 30 | validations: 31 | required: true 32 | - type: markdown 33 | attributes: 34 | value: | 35 | You can add more additional information in the form of comments after creating an issue, such as screenshots of the controller. 36 | -------------------------------------------------------------------------------- /src/router/DualSenseEdge/views/_Profile/ProfileRenameInput.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/r3-twotone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/b-twotone-bordered.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/rb-twotone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/components/base/DouSwitch.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | 17 | 45 | -------------------------------------------------------------------------------- /src/composables/useToast/ToastContainer.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 25 | 26 | 45 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/r3-twotone-bordered.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/rb-twotone-bordered.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/utils/lang.util.ts: -------------------------------------------------------------------------------- 1 | import messages from '@intlify/unplugin-vue-i18n/messages' 2 | 3 | export function getAvailableLanguages(): string[] { 4 | return Object.keys(messages || {}) 5 | } 6 | 7 | export function getLangWithoutScript(lang: string): string { 8 | const langPart = lang.split('-') 9 | if (langPart.length === 2) { 10 | return `${langPart[0].toLowerCase()}-${langPart[1].toUpperCase()}` 11 | } 12 | else if (langPart.length === 3) { 13 | return `${langPart[0].toLowerCase()}-${langPart[2].toUpperCase()}` 14 | } 15 | return langPart[0].toLowerCase() 16 | } 17 | 18 | export function getPreferredLanguage(languages: readonly string[]): string { 19 | const lang = languages[0] 20 | const availableLanguages = getAvailableLanguages() 21 | if (availableLanguages.includes(lang)) { 22 | return lang 23 | } 24 | 25 | const langWithoutScript = getLangWithoutScript(lang) 26 | if (availableLanguages.includes(langWithoutScript)) { 27 | return langWithoutScript 28 | } 29 | 30 | const langType = lang.split('-')[0] 31 | const fallbackLang = availableLanguages.find(lang => lang.startsWith(langType)) 32 | if (fallbackLang) { 33 | return fallbackLang 34 | } 35 | return 'en-US' 36 | } 37 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | DualSense Tester 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 22 | 23 | 24 | 25 |
26 |
27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/composables/useLangSpecFont.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeRefOrGetter } from 'vue' 2 | import { toValue, watch } from 'vue' 3 | 4 | const defaultFontFamily = `'SN Pro', -apple-system, BlinkMacSystemFont, 'SF Pro Text', PingFang SC, 'Helvetica Neue', 'Helvetica', Hiragino Sans GB, Microsoft YaHei, sans-serif` 5 | 6 | export function getLangSpecFont(lang: string) { 7 | const font = { 8 | 'ar-SA': 'vazirmatn', 9 | 'ar-EG': 'vazirmatn', 10 | 'fa-IR': 'vazirmatn', 11 | }[lang] 12 | return `${font ? `${font},` : ''}${defaultFontFamily}` 13 | } 14 | 15 | function generateStyle(font: string) { 16 | return `\ 17 | :root { 18 | --default-fonts: ${font}; 19 | }\ 20 | ` 21 | } 22 | 23 | let styleWrapper = document.querySelector('#special-fonts')! 24 | if (!styleWrapper) { 25 | const style = document.createElement('style') 26 | style.id = 'special-fonts' 27 | document.head.appendChild(style) 28 | styleWrapper = style 29 | } 30 | 31 | export function useLangSpecFont(lang: MaybeRefOrGetter) { 32 | watch(() => toValue(lang), (lang) => { 33 | const style = generateStyle(getLangSpecFont(lang)) 34 | 35 | styleWrapper.textContent = style 36 | }, { 37 | immediate: true, 38 | flush: 'pre', 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /src/router/DualSense/_utils/offset.util.ts: -------------------------------------------------------------------------------- 1 | export const inputReportOffsetUSB = createInputReportOffset(true) 2 | export const inputReportOffsetBluetooth = createInputReportOffset(false) 3 | 4 | export function createInputReportOffset(usb: boolean) { 5 | const num = usb ? 0 : 1 6 | const offset = { 7 | analogStickLX: 0 + num, 8 | analogStickLY: 1 + num, 9 | analogStickRX: 2 + num, 10 | analogStickRY: 3 + num, 11 | analogTriggerL: 4 + num, 12 | analogTriggerR: 5 + num, 13 | sequenceNum: 6 + num, 14 | digitalKeys: 7 + num, 15 | incrementalNumber: 11 + num, 16 | gyroPitch: 15 + num, 17 | gyroYaw: 17 + num, 18 | gyroRoll: 19 + num, 19 | accelX: 21 + num, 20 | accelY: 23 + num, 21 | accelZ: 25 + num, 22 | motionTimeStamp: 27 + num, 23 | motionTemperature: 31 + num, 24 | touchData: 32 + num, 25 | atStatus0: 41 + num, 26 | atStatus1: 42 + num, 27 | hostTimestamp: 43 + num, 28 | atStatus2: 47 + num, 29 | deviceTimestamp: 48 + num, 30 | status0: 52 + num, 31 | status1: 53 + num, 32 | status2: 54 + num, 33 | aesCmac: 55 + num, 34 | seqTag: 0, 35 | crc32: 0, 36 | } 37 | 38 | if (!usb) { 39 | offset.crc32 = 73 40 | } 41 | 42 | return offset 43 | } 44 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/lb-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/router/DualSenseEdge/_utils/offset.util.ts: -------------------------------------------------------------------------------- 1 | export const inputReportOffsetUSB = createInputReportOffset(true) 2 | export const inputReportOffsetBluetooth = createInputReportOffset(false) 3 | 4 | export function createInputReportOffset(usb: boolean) { 5 | const num = usb ? 0 : 1 6 | const offset = { 7 | analogStickLX: 0 + num, 8 | analogStickLY: 1 + num, 9 | analogStickRX: 2 + num, 10 | analogStickRY: 3 + num, 11 | analogTriggerL: 4 + num, 12 | analogTriggerR: 5 + num, 13 | sequenceNum: 6 + num, 14 | digitalKeys: 7 + num, 15 | incrementalNumber: 11 + num, 16 | gyroPitch: 15 + num, 17 | gyroYaw: 17 + num, 18 | gyroRoll: 19 + num, 19 | accelX: 21 + num, 20 | accelY: 23 + num, 21 | accelZ: 25 + num, 22 | motionTimeStamp: 27 + num, 23 | motionTemperature: 31 + num, 24 | touchData: 32 + num, 25 | atStatus0: 41 + num, 26 | atStatus1: 42 + num, 27 | hostTimestamp: 43 + num, 28 | atStatus2: 47 + num, 29 | activeProfile: 48 + num, 30 | triggerLevel: 49 + num, 31 | status0: 52 + num, 32 | status1: 53 + num, 33 | status2: 54 + num, 34 | aesCmac: 55 + num, 35 | seqTag: 0, 36 | crc32: 0, 37 | } 38 | 39 | if (!usb) { 40 | offset.sequenceNum = 0 41 | offset.crc32 = 73 42 | } 43 | 44 | return offset 45 | } 46 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/ls-twotone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/base/DouNumberInput.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 26 | 27 | 45 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/ls-twotone-bordered.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/device-based-router/index.ts: -------------------------------------------------------------------------------- 1 | import type { ShallowRef } from 'vue' 2 | import type { BaseDeviceRouter, DeviceItemWithRouter } from './shared' 3 | import { shallowReactiveComputed } from '@/utils/reactive.util' 4 | 5 | export class RouterManager { 6 | private routers: BaseDeviceRouter[] = [] 7 | private aggregatedFilters: HIDDeviceFilter[] = [] 8 | 9 | register(router: BaseDeviceRouter) { 10 | this.routers.push(router) 11 | this.aggregatedFilters.push(...router.filters) 12 | } 13 | 14 | get filters() { 15 | return this.aggregatedFilters 16 | } 17 | 18 | async match(device: HIDDevice) { 19 | for (const router of this.routers) { 20 | if (await router.match(device)) { 21 | return router 22 | } 23 | } 24 | return undefined 25 | } 26 | 27 | reactiveViews(deviceItemRef: ShallowRef) { 28 | return shallowReactiveComputed(() => { 29 | return { 30 | connectWidgetPanels: deviceItemRef.value?.router.connectWidgetPanels?.(deviceItemRef.value), 31 | modelPanel: deviceItemRef.value?.router.modelPanel(deviceItemRef.value), 32 | visualizerPanels: deviceItemRef.value?.router.visualizerPanels?.(deviceItemRef.value), 33 | widgetPanels: deviceItemRef.value?.router.widgetPanels?.(deviceItemRef.value), 34 | } 35 | }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/crowdin-contributors.yml: -------------------------------------------------------------------------------- 1 | name: Crowdin Contributors Action 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | schedule: 7 | - cron: '0 */12 * * *' 8 | workflow_dispatch: 9 | 10 | jobs: 11 | crowdin-contributors: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: Generate Crowdin Contributors table 18 | uses: andrii-bodnar/action-crowdin-contributors@v2 19 | with: 20 | contributors_per_line: 8 21 | include_languages: true 22 | max_contributors: 32 23 | image_size: 64 24 | min_words_contributed: 1 25 | crowdin_project_link: https://crowdin.com/project/dualsense-tester 26 | env: 27 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} 28 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} 29 | 30 | - name: Create Pull Request 31 | uses: peter-evans/create-pull-request@v6 32 | with: 33 | title: Update Crowdin Contributors table 34 | body: By [action-crowdin-contributors](https://github.com/andrii-bodnar/action-crowdin-contributors) GitHub action 35 | commit-message: Update Crowdin Contributors table 36 | committer: Crowdin Bot 37 | branch: crowdin-contributors/patch 38 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/rt-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /plugins/html-plugin/index.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from 'vite' 2 | import { readFileSync } from 'node:fs' 3 | import path from 'node:path' 4 | import process from 'node:process' 5 | import { fileURLToPath } from 'node:url' 6 | 7 | function htmlPlugin(): Plugin { 8 | if (process.env.NODE_ENV === 'development') { 9 | return { 10 | name: 'html-transform', 11 | } 12 | } 13 | const __filename = fileURLToPath(import.meta.url) 14 | const __dirname = path.dirname(__filename) 15 | 16 | const buildEnv = process.env.BUILD_ENV 17 | const vercelEnv = process.env.VERCEL_ENV 18 | 19 | let ref = 'unknown' 20 | 21 | if (buildEnv === 'docker') { 22 | ref = 'docker' 23 | } 24 | else if (vercelEnv === 'production') { 25 | ref = 'saas' 26 | } 27 | 28 | const umamiTemplate = readFileSync(path.resolve(__dirname, './umami.html'), 'utf-8') 29 | 30 | const umamiScript = umamiTemplate.replace('%REF%', ref) 31 | 32 | let ga4Template = '' 33 | 34 | if (ref === 'saas') { 35 | ga4Template = readFileSync(path.resolve(__dirname, './ga4.html'), 'utf-8') 36 | } 37 | 38 | const finalScript = `${umamiScript}${ga4Template}` 39 | 40 | return { 41 | name: 'html-transform', 42 | transformIndexHtml(html) { 43 | return html.replace( 44 | //, 45 | finalScript, 46 | ) 47 | }, 48 | } 49 | } 50 | 51 | export default htmlPlugin 52 | -------------------------------------------------------------------------------- /packages/fancy-controller/scripts/build.ts: -------------------------------------------------------------------------------- 1 | import { 2 | cleanupSVG, 3 | importDirectory, 4 | isEmptyColor, 5 | parseColors, 6 | runSVGO, 7 | writeJSONFile, 8 | } from '@iconify/tools' 9 | 10 | (async () => { 11 | const iconSet = await importDirectory('sets', { 12 | prefix: 'fancy-controller', 13 | }) 14 | 15 | iconSet.forEach((name, type) => { 16 | if (type !== 'icon') { 17 | return 18 | } 19 | 20 | const svg = iconSet.toSVG(name) 21 | if (!svg) { 22 | // Invalid icon 23 | iconSet.remove(name) 24 | return 25 | } 26 | 27 | try { 28 | cleanupSVG(svg) 29 | 30 | parseColors(svg, { 31 | defaultColor: 'currentColor', 32 | callback: (attr, colorStr, color) => { 33 | return !color || isEmptyColor(color) 34 | ? colorStr 35 | : 'currentColor' 36 | }, 37 | }) 38 | 39 | runSVGO(svg, { 40 | }) 41 | } 42 | catch (err) { 43 | console.error(`Error parsing ${name}:`, err) 44 | iconSet.remove(name) 45 | return 46 | } 47 | 48 | iconSet.fromSVG(name, svg) 49 | }) 50 | 51 | iconSet.suffixes = { 52 | '': 'Other', 53 | 'twotone': 'Two-tone', 54 | 'solid': 'Solid', 55 | 'twotone-bordered': 'Two-tone bordered', 56 | 'outline': 'Outline', 57 | } 58 | 59 | const result = iconSet.export() 60 | writeJSONFile('icons.json', result) 61 | })() 62 | -------------------------------------------------------------------------------- /src/components/common/VisualizerPanelShell.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/r1-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/rs-twotone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/fn-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/rs-twotone-bordered.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/common/GitVersion.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 44 | 45 | 54 | -------------------------------------------------------------------------------- /src/components/common/WidgetShell.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://openapi.vercel.sh/vercel.json", 3 | "framework": "vite", 4 | "installCommand": "bun install --frozen-lockfile", 5 | "buildCommand": "bun run build-only", 6 | "headers": [ 7 | { 8 | "source": "/(.*).html", 9 | "headers": [ 10 | { 11 | "key": "Cache-Control", 12 | "value": "public, max-age=0, must-revalidate" 13 | } 14 | ] 15 | }, 16 | { 17 | "source": "/sw.js", 18 | "headers": [ 19 | { 20 | "key": "Cache-Control", 21 | "value": "public, max-age=0, must-revalidate" 22 | } 23 | ] 24 | }, 25 | { 26 | "source": "/manifest.webmanifest", 27 | "headers": [ 28 | { 29 | "key": "Content-Type", 30 | "value": "application/manifest+json" 31 | } 32 | ] 33 | }, 34 | { 35 | "source": "/assets/(.*)", 36 | "headers": [ 37 | { 38 | "key": "Cache-Control", 39 | "value": "max-age=31536000, immutable" 40 | } 41 | ] 42 | }, 43 | { 44 | "source": "/(.*)", 45 | "headers": [ 46 | { 47 | "key": "X-Content-Type-Options", 48 | "value": "nosniff" 49 | }, 50 | { 51 | "key": "X-Frame-Options", 52 | "value": "DENY" 53 | }, 54 | { 55 | "key": "X-XSS-Protection", 56 | "value": "1; mode=block" 57 | } 58 | ] 59 | } 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /src/components/OverlayHeader.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 22 | 23 | 57 | -------------------------------------------------------------------------------- /src/components/common/GroupedButton.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 32 | 33 | 49 | -------------------------------------------------------------------------------- /src/components/common/SwitchBox.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 25 | 26 | 60 | -------------------------------------------------------------------------------- /src/composables/useMagicTeleport.ts: -------------------------------------------------------------------------------- 1 | import type { ShallowReactive, VNode } from 'vue' 2 | import { defineComponent, onScopeDispose, shallowReactive } from 'vue' 3 | 4 | interface MagicTeleportItem { 5 | refCount: number 6 | vNode: VNode | VNode[] | null 7 | } 8 | 9 | const magicTeleportStore = new Map>() 10 | 11 | export function useMagicTeleport(name: string) { 12 | function useTeleportItem() { 13 | let item = magicTeleportStore.get(name) 14 | if (item) { 15 | item.refCount++ 16 | } 17 | else { 18 | item = shallowReactive({ refCount: 1, vNode: null }) 19 | magicTeleportStore.set(name, item) 20 | } 21 | onScopeDispose(() => { 22 | item.refCount-- 23 | if (item.refCount <= 0) { 24 | magicTeleportStore.delete(name) 25 | } 26 | }) 27 | return item 28 | } 29 | 30 | return { 31 | /** 32 | * The target element to teleport to. 33 | */ 34 | MagicTeleportView: defineComponent(() => { 35 | const item = useTeleportItem() 36 | return () => { 37 | if (item.vNode) { 38 | return item.vNode 39 | } 40 | return null 41 | } 42 | }), 43 | /** 44 | * The teleport component to use. 45 | */ 46 | MagicTeleport: defineComponent((_, ctx) => { 47 | const item = useTeleportItem() 48 | 49 | return () => { 50 | item.vNode = ctx.slots.default ? ctx.slots.default() : null 51 | return null 52 | } 53 | }), 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /config/crowdin.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | 3 | function percentageToNumber(percentage: number) { 4 | return percentage / 100 5 | } 6 | 7 | const isDev = process.env.NODE_ENV === 'development' 8 | 9 | export async function crowdinDefine(env: Record) { 10 | try { 11 | if (isDev) { 12 | return { 13 | __CROWDIN_PROGRESS__: JSON.stringify({}), 14 | } 15 | } 16 | if (!env.CROWDIN_API_KEY) { 17 | console.error('CROWDIN_API_KEY is not set') 18 | return { 19 | __CROWDIN_PROGRESS__: JSON.stringify({}), 20 | } 21 | } 22 | const resp = await fetch('https://api.crowdin.com/api/v2/projects/758431/languages/progress?limit=500', { 23 | headers: { 24 | Authorization: `Bearer ${env.CROWDIN_API_KEY}`, 25 | }, 26 | }) 27 | const data = await resp.json() as any 28 | const result = data.data.map((i: any) => ({ 29 | locale: i.data.language.locale, 30 | translationProgress: percentageToNumber(i.data.translationProgress), 31 | approvalProgress: percentageToNumber(i.data.approvalProgress), 32 | })) 33 | const resultMap: Record = {} 34 | for (const item of result) { 35 | resultMap[item.locale as string] = item 36 | } 37 | return { 38 | __CROWDIN_PROGRESS__: JSON.stringify(resultMap), 39 | } 40 | } 41 | catch (error) { 42 | console.error('Failed to fetch crowdin data', error) 43 | return { 44 | __CROWDIN_PROGRESS__: JSON.stringify({}), 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/router/DualSenseEdge/views/HideInConfigModeLayout.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/components/LangSwitcher.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 47 | 48 | 53 | -------------------------------------------------------------------------------- /src/router/DualSenseEdge/views/_Profile/ProfileSwitchButton.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/components/GyroValueBar.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 40 | 41 | 51 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/rb-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/router/DualSense/views/_visualizerPanel/AccelView.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/router/DualSenseEdge/views/_visualizerPanel/AccelView.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/components/common/ConditionShell.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/router/DualSense/views/_visualizerPanel/GyroView.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/router/DualShockV2/views/_visualizerPanel/AccelView.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 40 | 41 | 46 | -------------------------------------------------------------------------------- /src/router/DualSenseEdge/views/_visualizerPanel/GyroView.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/router/DualShockV2/views/_visualizerPanel/GyroView.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/components/common/PWAPrompt.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 67 | -------------------------------------------------------------------------------- /src/utils/reactive.util.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeRef, Ref, ShallowReactive } from 'vue' 2 | import { computed, isRef, ref, shallowReactive, unref } from 'vue' 3 | 4 | export function refWithHandler(value: T, handler?: (value: T) => void) { 5 | const refValue = ref(value) 6 | return computed({ 7 | get() { 8 | return refValue.value 9 | }, 10 | set(value) { 11 | refValue.value = value 12 | handler?.(value) 13 | }, 14 | }) 15 | } 16 | 17 | export function fastGetRef(value: T): T['value'] { 18 | return (value as any)._rawValue 19 | } 20 | 21 | export function toShallowReactive( 22 | objectRef: MaybeRef, 23 | ): ShallowReactive { 24 | if (!isRef(objectRef)) { 25 | return shallowReactive(objectRef) 26 | } 27 | 28 | const proxy = new Proxy({}, { 29 | get(_, p, receiver) { 30 | return unref(Reflect.get(objectRef.value, p, receiver)) 31 | }, 32 | set(_, p, value) { 33 | if (isRef((objectRef.value as any)[p]) && !isRef(value)) { 34 | (objectRef.value as any)[p].value = value 35 | } 36 | else { 37 | (objectRef.value as any)[p] = value 38 | } 39 | return true 40 | }, 41 | deleteProperty(_, p) { 42 | return Reflect.deleteProperty(objectRef.value, p) 43 | }, 44 | has(_, p) { 45 | return Reflect.has(objectRef.value, p) 46 | }, 47 | ownKeys() { 48 | return Object.keys(objectRef.value) 49 | }, 50 | getOwnPropertyDescriptor() { 51 | return { 52 | enumerable: true, 53 | configurable: true, 54 | } 55 | }, 56 | }) 57 | 58 | return shallowReactive(proxy) as ShallowReactive 59 | } 60 | 61 | export function shallowReactiveComputed(fn: () => T): ShallowReactive { 62 | return toShallowReactive(computed(fn)) 63 | } 64 | -------------------------------------------------------------------------------- /src/components/common/HexPreview.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 56 | 57 | 60 | -------------------------------------------------------------------------------- /packages/fancy-controller/sets/Button/l2-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/MainHeader.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 38 | 39 | 63 | -------------------------------------------------------------------------------- /src/composables/useEventBus.ts: -------------------------------------------------------------------------------- 1 | import { inject, onScopeDispose, provide } from 'vue' 2 | import { eventBusLogger } from '@/utils/logger.util' 3 | 4 | const __EVENT_BUS_PROVIDE__ = '__EVENT_BUS_PROVIDE__' 5 | 6 | type EventMap = Map> 7 | 8 | export function useEventBusProvider() { 9 | const events: EventMap = new Map() 10 | 11 | provide(__EVENT_BUS_PROVIDE__, events) 12 | } 13 | 14 | /** 15 | * Use case: 16 | * const [useEventBusRegister, eventBusEmit] = useEventBus({ 17 | * 'event-name': [param1: string, param2: number] 18 | * }) 19 | * 20 | * useEventBusRegister('event-name', (param1, param2) => { 21 | * console.log(param1, param2) 22 | * }) 23 | * 24 | * 25 | * eventBusEmit('event-name', 'param1', 2) 26 | */ 27 | 28 | export function useEventBus>() { 29 | const useEventBusRegister = (eventName: EventName, callback: (...args: EventDefs[EventName]) => void) => { 30 | const eventBus = inject(__EVENT_BUS_PROVIDE__) 31 | eventBusLogger.debug(`Registering event`, eventName, eventBus) 32 | if (!eventBus?.has(eventName)) { 33 | eventBus?.set(eventName, new Set()) 34 | } 35 | eventBus?.get(eventName)?.add(callback) 36 | 37 | onScopeDispose(() => { 38 | eventBus?.get(eventName)?.delete(callback) 39 | }) 40 | } 41 | 42 | const useEventBusEmit = () => { 43 | const eventBus = inject(__EVENT_BUS_PROVIDE__) 44 | eventBusLogger.debug(`Create event emitter`, eventBus) 45 | return (eventName: EventName, ...args: EventDefs[EventName]) => { 46 | eventBus?.get(eventName)?.forEach((callback) => { 47 | callback(...args) 48 | }) 49 | } 50 | } 51 | 52 | return [useEventBusRegister, useEventBusEmit] as const 53 | } 54 | -------------------------------------------------------------------------------- /src/AppInner.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/components/common/HoldActiveButton.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 69 | 70 | 83 | -------------------------------------------------------------------------------- /src/assets/fonts.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'SN Pro'; 3 | font-style: normal; 4 | font-weight: 100 900; 5 | font-stretch: 100%; 6 | font-display: swap; 7 | src: url(/fonts/sn-pro-var.woff2) format('woff2'); 8 | } 9 | 10 | /* arabic */ 11 | @font-face { 12 | font-family: 'Vazirmatn'; 13 | font-style: normal; 14 | font-weight: 100 900; 15 | font-display: swap; 16 | src: url(/fonts/vazirmatn-arabic.woff2) format('woff2'); 17 | unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0897-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC, U+102E0-102FB, U+10E60-10E7E, U+10EC2-10EC4, U+10EFC-10EFF, U+1EE00-1EE03, U+1EE05-1EE1F, U+1EE21-1EE22, U+1EE24, U+1EE27, U+1EE29-1EE32, U+1EE34-1EE37, U+1EE39, U+1EE3B, U+1EE42, U+1EE47, U+1EE49, U+1EE4B, U+1EE4D-1EE4F, U+1EE51-1EE52, U+1EE54, U+1EE57, U+1EE59, U+1EE5B, U+1EE5D, U+1EE5F, U+1EE61-1EE62, U+1EE64, U+1EE67-1EE6A, U+1EE6C-1EE72, U+1EE74-1EE77, U+1EE79-1EE7C, U+1EE7E, U+1EE80-1EE89, U+1EE8B-1EE9B, U+1EEA1-1EEA3, U+1EEA5-1EEA9, U+1EEAB-1EEBB, U+1EEF0-1EEF1; 18 | } 19 | 20 | /* latin-ext */ 21 | @font-face { 22 | font-family: 'Vazirmatn'; 23 | font-style: normal; 24 | font-weight: 100 900; 25 | font-display: swap; 26 | src: url(/fonts/vazirmatn-latin-ext.woff2) format('woff2'); 27 | unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; 28 | } 29 | 30 | /* latin */ 31 | @font-face { 32 | font-family: 'Vazirmatn'; 33 | font-style: normal; 34 | font-weight: 100 900; 35 | font-display: swap; 36 | src: url(/fonts/vazirmatn-latin.woff2) format('woff2'); 37 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 38 | } -------------------------------------------------------------------------------- /src/components/MainFooter.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 43 | 44 | 60 | -------------------------------------------------------------------------------- /src/utils/format.util.ts: -------------------------------------------------------------------------------- 1 | import { shiftJISDecoder } from './decoder.util' 2 | 3 | export function numberToHex(value: number | bigint, length = 0) { 4 | return value.toString(16).padStart(length, '0').toUpperCase() 5 | } 6 | 7 | export function numberToXHex(value: number | bigint, length = 0) { 8 | return `0x${numberToHex(value, length)}` 9 | } 10 | 11 | export function mapDataViewToU8Hex(dataView: DataView, littleEndian = false) { 12 | const result: string[] = [] 13 | for (let i = 0; i < dataView.byteLength; i++) { 14 | result.push(numberToHex(dataView.getUint8(i), 2)) 15 | } 16 | return littleEndian ? result.reverse().join('') : result.join('') 17 | } 18 | 19 | export function decodeShiftJIS(dataView?: DataView) { 20 | if (!dataView) { 21 | return '' 22 | } 23 | // oxlint-disable-next-line no-control-regex 24 | return shiftJISDecoder.decode(dataView).replace(/\0/g, '') 25 | } 26 | 27 | export function numberToMacAddress(value: bigint) { 28 | return value.toString(16).padStart(12, '0').toUpperCase().replace(/(.{2})/g, '$1:').slice(0, -1) 29 | } 30 | 31 | export function pairedValue(left: string | undefined, right: string | undefined, formatter?: (value: string | undefined) => string) { 32 | let finalString = '' 33 | if (left) { 34 | finalString += `${formatter?.(left) ?? left} (L)\n` 35 | } 36 | if (right) { 37 | finalString += `${formatter?.(right) ?? right} (R)\n` 38 | } 39 | return finalString.trim() 40 | } 41 | 42 | export function notAllFalsy(...args: unknown[]) { 43 | return args.some(arg => !!arg) 44 | } 45 | 46 | export function bitShiftByte(value: number, shift: number) { 47 | return (value << shift) & 0xFF 48 | } 49 | 50 | export function formatAccel(value: number): number | string { 51 | const ACCEL_RES_PER_G = 8192 52 | const STANDARD_GRAVITY = 9.80665 53 | 54 | const accel = (value / ACCEL_RES_PER_G) * STANDARD_GRAVITY 55 | 56 | if (accel === 0) { 57 | return accel 58 | } 59 | else { 60 | return accel.toFixed(5) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Production Release 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | release: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Checkout repository (with tags) 12 | uses: actions/checkout@v4 13 | with: 14 | fetch-depth: 0 15 | fetch-tags: true 16 | 17 | - name: Configure Git 18 | run: | 19 | git config user.name "GitHub Actions" 20 | git config user.email "actions@github.com" 21 | 22 | - name: Force push main to release 23 | run: | 24 | echo "🔄 Forcing push from main to release branch" 25 | git push origin main:release --force 26 | 27 | - name: Get latest tag 28 | id: get_tag 29 | run: | 30 | git checkout release 31 | # Get all tags 32 | git fetch --tags 33 | 34 | latest_tag=$(git tag --sort=-v:refname | head -n 1) 35 | if [ -z "$latest_tag" ]; then 36 | latest_tag="v0.0.0" 37 | fi 38 | echo "Latest tag: $latest_tag" 39 | echo "latest_tag=$latest_tag" >> $GITHUB_OUTPUT 40 | 41 | - name: Calculate next tag 42 | id: next_tag 43 | run: | 44 | old_tag="${{ steps.get_tag.outputs.latest_tag }}" 45 | echo "Calculating next version from $old_tag" 46 | 47 | # Split 48 | IFS='.' read -r major minor patch <<< "${old_tag#v}" 49 | 50 | new_patch=$((patch + 1)) 51 | new_tag="v${major}.${minor}.${new_patch}" 52 | 53 | echo "New tag will be: $new_tag" 54 | echo "new_tag=$new_tag" >> $GITHUB_OUTPUT 55 | 56 | - name: Create and push new tag 57 | run: | 58 | tag=${{ steps.next_tag.outputs.new_tag }} 59 | git tag "$tag" 60 | git push origin "$tag" 61 | 62 | - name: ✅ Done 63 | run: | 64 | echo "🎉 Release branch updated!" 65 | echo "📦 Version tagged: ${{ steps.next_tag.outputs.new_tag }}" 66 | echo "🕒 Time: $(date)" 67 | -------------------------------------------------------------------------------- /uno.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, presetIcons, presetWind3, transformerDirectives, transformerVariantGroup } from 'unocss' 2 | 3 | export default defineConfig({ 4 | details: true, 5 | presets: [ 6 | presetWind3({ 7 | dark: 'class', 8 | }), 9 | presetIcons({ 10 | collections: { 11 | 'mingcute': () => import('@iconify-json/mingcute/icons.json').then(i => i.default), 12 | 'icon-park-twotone': () => import('@iconify-json/icon-park-twotone/icons.json').then(i => i.default), 13 | 'fancy-controller': () => import('fancy-controller/icons.json').then(i => i.default), 14 | }, 15 | }), 16 | ], 17 | transformers: [transformerVariantGroup(), transformerDirectives()], 18 | theme: { 19 | colors: { 20 | primary: { 21 | light: '#e8effb', 22 | DEFAULT: '#2f81f7', 23 | dark: '#071f40', 24 | }, 25 | }, 26 | fontFamily: { 27 | mono: ['Noto Sans Mono', 'ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', '"Liberation Mono"', '"Courier New"'], 28 | }, 29 | containers: { 30 | sm: '(min-width: 24rem)', 31 | }, 32 | }, 33 | shortcuts: { 34 | 'dou-sc-capsule': 35 | 'px-2 lh-1.1em flex-shrink-0 text-primary/80 dou-sc-colorborder rounded-full whitespace-nowrap', 36 | 'dou-sc-btn': 37 | 'transition bg-transparent dou-sc-colorborder text-primary px-4 py-1 rounded-full transform-gpu flex items-center gap-1 hover:(bg-primary text-white) active:(scale-90) disabled:(filter-grayscale bg-transparent! scale-100! text-primary! cursor-not-allowed)', 38 | 'dou-sc-container': 'dou-sc-autoborder rounded-32px px-3 py-3', 39 | 'dou-sc-autoborder': 'border-1 border-gray-3 dark-border-gray-6', 40 | 'dou-sc-colorborder': 'border-1.5 border-primary/20 dark-border-primary/50', 41 | 'dou-sc-autobg': 'bg-primary/20 dark-bg-primary/50', 42 | 'dou-sc-title': 'text-xl font-bold text-primary lh-1em', 43 | 'dou-sc-subtitle': 'text-xl font-bold text-primary lh-1.2em my-2', 44 | 'dou-sc-link': 'text-primary hover:underline', 45 | }, 46 | }) 47 | --------------------------------------------------------------------------------