feat: accessibility settings, subtitle/QTE improvements, docs update
This commit is contained in:
179
src/components/AccessibilitySettings.vue
Normal file
179
src/components/AccessibilitySettings.vue
Normal file
@@ -0,0 +1,179 @@
|
||||
<script setup lang="ts">
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
|
||||
const store = useGameStore()
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
}>()
|
||||
|
||||
const fontSizeOptions = [20, 24, 28, 32]
|
||||
const bgAlphaOptions = [0, 0.3, 0.5, 0.7, 0.9]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="settings-overlay" @click.self="emit('close')" @keydown.escape="emit('close')">
|
||||
<div class="settings-panel">
|
||||
<h2 class="settings-title">设置</h2>
|
||||
|
||||
<div class="settings-body">
|
||||
<div class="setting-row">
|
||||
<span class="setting-label">字幕字号</span>
|
||||
<select :value="store.subFontSize" @change="store.setSubFontSize(+($event.target as HTMLSelectElement).value)">
|
||||
<option v-for="s in fontSizeOptions" :key="s" :value="s">{{ s }}px</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<span class="setting-label">字幕背景</span>
|
||||
<select :value="store.subBgAlpha" @change="store.setSubBgAlpha(+($event.target as HTMLSelectElement).value)">
|
||||
<option :value="0">无</option>
|
||||
<option v-for="a in bgAlphaOptions.filter(v => v > 0)" :key="a" :value="a">{{ (a * 100) + '%' }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<span class="setting-label">QTE 时限放宽 (×1.5)</span>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" :checked="store.qteTimeRelax" @change="store.setQteTimeRelax(($event.target as HTMLInputElement).checked)" />
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<span class="setting-label">QTE 按键简化(仅空格)</span>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" :checked="store.qteSingleKey" @change="store.setQteSingleKey(($event.target as HTMLInputElement).checked)" />
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<span class="setting-label">防误触延迟 (0.5s)</span>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" :checked="store.antiMistap" @change="store.setAntiMistap(($event.target as HTMLInputElement).checked)" />
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<span class="setting-label">可暂停 (Space)</span>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" :checked="store.pauseEnabled" @change="store.setPauseEnabled(($event.target as HTMLInputElement).checked)" />
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="settings-close" @click="emit('close')">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.settings-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.settings-panel {
|
||||
background: #1a1a2e;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 10px;
|
||||
padding: 36px 40px;
|
||||
min-width: 400px;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.settings-title {
|
||||
text-align: center;
|
||||
font-size: 22px;
|
||||
font-weight: 400;
|
||||
color: #ddd;
|
||||
letter-spacing: 3px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.settings-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.setting-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
font-size: 13px;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
select {
|
||||
padding: 4px 10px;
|
||||
font-size: 13px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 3px;
|
||||
color: #ddd;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.toggle input { display: none; }
|
||||
|
||||
.toggle-slider {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 11px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.toggle-slider::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
left: 3px;
|
||||
top: 3px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.toggle input:checked + .toggle-slider { background: #4caf50; }
|
||||
.toggle input:checked + .toggle-slider::after { transform: translateX(18px); }
|
||||
|
||||
.settings-close {
|
||||
display: block;
|
||||
margin: 24px auto 0;
|
||||
padding: 10px 36px;
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.settings-close:hover { background: rgba(255, 255, 255, 0.1); color: #ccc; }
|
||||
</style>
|
||||
@@ -2,6 +2,7 @@
|
||||
import { ref, watch, nextTick } from 'vue'
|
||||
import type { Choice } from '@engine/types'
|
||||
import { useI18n } from '@/composables/useI18n'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
|
||||
const props = defineProps<{
|
||||
choices: Choice[]
|
||||
@@ -15,8 +16,11 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const store = useGameStore()
|
||||
const focusIndex = ref(0)
|
||||
const btnRefs = ref<(HTMLButtonElement | null)[]>([])
|
||||
const choiceEnabled = ref(!store.antiMistap)
|
||||
let enableTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
function timerPercent(): number {
|
||||
if (props.timerTotal <= 0) return 0
|
||||
@@ -34,6 +38,13 @@ function setRef(el: HTMLButtonElement | null, index: number) {
|
||||
|
||||
watch(() => props.choices.length, async (len) => {
|
||||
if (len > 0) {
|
||||
if (store.antiMistap) {
|
||||
choiceEnabled.value = false
|
||||
if (enableTimer) clearTimeout(enableTimer)
|
||||
enableTimer = setTimeout(() => { choiceEnabled.value = true }, 500)
|
||||
} else {
|
||||
choiceEnabled.value = true
|
||||
}
|
||||
focusIndex.value = 0
|
||||
await nextTick()
|
||||
btnRefs.value[0]?.focus()
|
||||
@@ -55,6 +66,7 @@ function onKeydown(e: KeyboardEvent, index: number) {
|
||||
}
|
||||
|
||||
function handleChoose(index: number) {
|
||||
if (!choiceEnabled.value) return
|
||||
const choice = props.choices[index]
|
||||
if (choice?.prompt) {
|
||||
emit('prompt', choice.prompt)
|
||||
@@ -74,7 +86,7 @@ function handleChoose(index: number) {
|
||||
<span class="timer-text">{{ timerRemaining.toFixed(1) }}s</span>
|
||||
</div>
|
||||
<div class="choice-prompt">{{ t('ui.choose') }}</div>
|
||||
<div class="choice-list">
|
||||
<div class="choice-list" :class="{ disabled: !choiceEnabled }">
|
||||
<button
|
||||
v-for="(choice, index) in choices"
|
||||
:key="index"
|
||||
@@ -145,6 +157,11 @@ function handleChoose(index: number) {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.choice-list.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.choice-btn {
|
||||
position: relative;
|
||||
padding: 14px 24px;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { useGameStore } from '@/stores/gameStore'
|
||||
|
||||
interface SubCue {
|
||||
start: number
|
||||
@@ -14,6 +15,7 @@ const props = defineProps<{
|
||||
lang?: string
|
||||
}>()
|
||||
|
||||
const store = useGameStore()
|
||||
const cues = ref<SubCue[]>([])
|
||||
const currentText = ref('')
|
||||
const loadedUrl = ref('')
|
||||
@@ -96,7 +98,13 @@ function vttTimeToSeconds(ts: string): number {
|
||||
|
||||
<template>
|
||||
<div class="subtitles" v-if="currentText">
|
||||
<div class="sub-text">{{ currentText }}</div>
|
||||
<div
|
||||
class="sub-text"
|
||||
:style="{
|
||||
fontSize: store.subFontSize + 'px',
|
||||
background: `rgba(0, 0, 0, ${store.subBgAlpha})`,
|
||||
}"
|
||||
>{{ currentText }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user