feat: onboarding

This commit is contained in:
2025-04-18 15:00:52 +00:00
parent 5d2d407659
commit e87b661f08
24 changed files with 408 additions and 139 deletions

View File

@ -0,0 +1,3 @@
If you're stuck on this screen, restart the game.
If the problem persists, <a href="https://gitea.tendokyu.moe/Dniel97/SEGAguide/wiki/FAQ#game-is-stuck-at-checking-distribution-server" target="_blank">check your network configuration</a>

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

8
public/help-finale.md Normal file
View File

@ -0,0 +1,8 @@
You can access this page any time by right-clicking the START button.
Additional resources:
- <a href="https://gitea.tendokyu.moe/Dniel97/SEGAguide/wiki/FAQ" target="_blank">SEGAguide</a>
- <a href="https://two-torial.xyz/" target="_blank">two-torial</a>
## Have fun

View File

@ -0,0 +1,3 @@
You also have to calibrate the lever, or you may get the error 3301.
Go to lever settings (<span class="bg-black text-white">レバー設定</span>), move the lever to both edges, then press "end" (<span class="bg-black text-white">終了</span>) and "save" (<span class="bg-black text-white">保存する</span>).

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View File

@ -0,0 +1,3 @@
You might get stuck on this screen for several minutes. _This is normal_. The game just takes a long time to load data.
If you install <code>7EVENDAYSHOLIDAYS/LoadBoost</code>, subsequent launches will be much faster.

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

7
public/help-standard.md Normal file
View File

@ -0,0 +1,7 @@
You might get stuck on the following screen:
<div class="p-2 mt-1 mb-1 bg-black text-white">Aグループの基準機から設定を取得</div>
In which case, you should go to the test menu, and in game settings <span class="bg-black text-white">ゲーム設定</span> switch from "follow the standard machine" <span class="bg-black text-white">基準機に従う</span> to "standard machine" <span class="bg-black text-white">基準機</span>.
The test menu can be accessed with %TESTMENU%.

View File

@ -334,8 +334,8 @@ fn open_window(apph: AppHandle) -> anyhow::Result<()> {
let config = apph.config().clone();
tauri::WebviewWindowBuilder::new(&apph, "main", tauri::WebviewUrl::App("index.html".into()))
.title(format!("STARTLINER {}", config.version.unwrap_or_default()))
.inner_size(900f64, 480f64)
.min_inner_size(900f64, 480f64)
.inner_size(900f64, 600f64)
.min_inner_size(900f64, 600f64)
.build()?;
Ok(())

View File

@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "STARTLINER",
"version": "0.10.1",
"version": "0.11.0",
"identifier": "zip.patafour.startliner",
"build": {
"beforeDevCommand": "bun run dev",

View File

@ -65,7 +65,7 @@ const filePick = async () => {
<template>
<Button v-if="!exists" icon="pi pi-plus" size="small" @click="filePick" />
<div v-else>
<div class="primitive-base" v-else>
<Button
v-if="exists"
icon="pi pi-pen-to-square"
@ -102,12 +102,20 @@ const filePick = async () => {
font-family: monospace;
white-space: nowrap;
position: fixed;
top: 10vh;
left: 10vw;
height: 80vh;
width: 80vw;
top: 50%;
left: 50%;
height: 500px;
width: 800px;
margin-left: -400px;
margin-top: -250px;
z-index: 1000;
padding: 20px;
border-radius: 20px;
background-color: #151515;
color: #ddd;
}
.primitive-base ::-webkit-scrollbar {
display: none;
}
</style>

View File

@ -16,7 +16,7 @@ invoke('get_changelog').then((s) => (changelog.value = s as string));
O.N.G.E.K.I. and CHUNITHM.
<h1>Changelog</h1>
<ScrollPanel style="height: 200px">
<div class="changelog">
<div class="markdown">
<vue-markdown-it
:source="changelog"
:options="{ typographer: true, breaks: true }"
@ -44,13 +44,20 @@ h1 {
font-size: 1.7rem;
}
.changelog h2 {
.markdown h3 {
font-size: 1.2rem;
}
.markdown h2 {
font-size: 1.4rem;
}
.changelog ul {
.markdown ul {
list-style-type: circle;
}
.changelog li {
.markdown li {
margin-left: 40px;
}
.markdown a {
text-decoration: underline;
}
</style>

View File

@ -1,6 +1,7 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import InputText from 'primevue/inputtext';
import { fromKeycode, toKeycode } from '../keyboard';
import { usePrfStore } from '../stores';
import { OngekiButtons } from '../types';
@ -127,126 +128,6 @@ const getKey = (key: keyof OngekiButtons, index?: number): any =>
return keycode && fromKeycode(keycode) ? fromKeycode(keycode) : '';
});
const KEY_MAP: { [key: number]: string } = {
1: 'M1',
2: 'M2',
4: 'M3',
5: 'M4',
6: 'M5',
8: 'Backspace',
9: 'Tab',
12: 'Clear',
13: 'Enter',
19: 'Pause',
20: 'CapsLock',
27: 'Escape',
32: 'Space',
33: 'PageUp',
34: 'PageDown',
35: 'End',
36: 'Home',
37: 'ArrowLeft',
38: 'ArrowUp',
39: 'ArrowRight',
40: 'ArrowDown',
45: 'Insert',
46: 'Delete',
48: 'Digit0',
49: 'Digit1',
50: 'Digit2',
51: 'Digit3',
52: 'Digit4',
53: 'Digit5',
54: 'Digit6',
55: 'Digit7',
56: 'Digit8',
57: 'Digit9',
65: 'KeyA',
66: 'KeyB',
67: 'KeyC',
68: 'KeyD',
69: 'KeyE',
70: 'KeyF',
71: 'KeyG',
72: 'KeyH',
73: 'KeyI',
74: 'KeyJ',
75: 'KeyK',
76: 'KeyL',
77: 'KeyM',
78: 'KeyN',
79: 'KeyO',
80: 'KeyP',
81: 'KeyQ',
82: 'KeyR',
83: 'KeyS',
84: 'KeyT',
85: 'KeyU',
86: 'KeyV',
87: 'KeyW',
88: 'KeyX',
89: 'KeyY',
90: 'KeyZ',
91: 'MetaLeft',
92: 'MetaRight',
93: 'ContextMenu',
96: 'Numpad0',
97: 'Numpad1',
98: 'Numpad2',
99: 'Numpad3',
100: 'Numpad4',
101: 'Numpad5',
102: 'Numpad6',
103: 'Numpad7',
104: 'Numpad8',
105: 'Numpad9',
106: 'NumpadMultiply',
107: 'NumpadAdd',
109: 'NumpadSubtract',
110: 'NumpadDecimal',
111: 'NumpadDivide',
112: 'F1',
113: 'F2',
114: 'F3',
115: 'F4',
116: 'F5',
117: 'F6',
118: 'F7',
119: 'F8',
120: 'F9',
121: 'F10',
122: 'F11',
123: 'F12',
144: 'NumLock',
145: 'ScrollLock',
160: 'ShiftLeft',
161: 'ShiftRight',
162: 'ControlLeft',
163: 'ControlRight',
164: 'AltLeft',
165: 'AltRight',
186: 'Semicolon',
187: 'Equal',
188: 'Comma',
189: 'Minus',
190: 'Period',
191: 'Slash',
192: 'Backquote',
219: 'BracketLeft',
220: 'Backslash',
221: 'BracketRight',
222: 'Quote',
};
const fromKeycode = (keyCode: number): string | null => {
return KEY_MAP[keyCode] ?? null;
};
const toKeycode = (key: string): number | null => {
const res = Object.entries(KEY_MAP).find(([_, v]) => v === key)?.[0];
return res ? parseInt(res) : null;
};
const props = defineProps({
small: Boolean,
tall: Boolean,

View File

@ -38,7 +38,7 @@ const iconSrc = computed(() => {
<label class="m-3 align-middle text grow z-5 h-50px">
<div>
<span class="text-lg">
{{ pkg?.name ?? 'Untitled' }}
{{ pkg?.name.replaceAll('_', ' ') ?? 'Untitled' }}
</span>
<span
v-if="pkg?.rmt?.deprecated"

View File

@ -0,0 +1,155 @@
<script setup lang="ts">
import { ComputedRef, computed, onMounted } from 'vue';
import Button from 'primevue/button';
import Carousel from 'primevue/carousel';
import Dialog from 'primevue/dialog';
import { fromKeycode } from '../keyboard';
import { useClientStore, usePrfStore } from '../stores';
import { prettyPrint } from '../util';
import { VueMarkdownIt } from '@f3ve/vue-markdown-it';
const prf = usePrfStore();
const client = useClientStore();
defineProps({
visible: Boolean,
firstTime: Boolean,
onFinish: Function,
});
interface Datum {
text: string;
image: string;
}
const game = computed(() => prf.current?.meta.game);
const processText = (s: string) => {
if (prf.current!.data.keyboard?.data.enabled) {
const testKey = prf.current!.data.keyboard?.data.test;
const readable = fromKeycode(testKey);
if (readable !== null) {
return s.replace(
'%TESTMENU%',
`${readable} or a button on the back of the controller`
);
}
}
return s.replace('%TESTMENU%', 'a button on the back of the controller');
};
const loadPage = async (title: string) => {
return {
text: await (await fetch(`/help-${title}.md`)).text(),
image: `help-${title}.png`,
};
};
let systemProcessing: Datum;
let standardOngeki: Datum;
let standardChunithm: Datum;
let lever: Datum;
let server: Datum;
let finaleOngeki: Datum;
let finaleChunithm: Datum;
const data: ComputedRef<Datum[]> = computed(() => {
const res = [];
switch (prf.current?.meta.game) {
case 'ongeki':
res.push(systemProcessing);
res.push(standardOngeki);
res.push(lever);
res.push(finaleOngeki);
break;
case 'chunithm':
res.push(standardChunithm);
res.push(server);
res.push(finaleChunithm);
break;
default:
break;
}
return res;
});
onMounted(async () => {
[standardOngeki, systemProcessing, lever, server, finaleOngeki] =
await Promise.all([
loadPage('standard'),
loadPage('ongeki-system-processing'),
loadPage('ongeki-lever'),
loadPage('chunithm-server'),
loadPage('finale'),
]);
standardOngeki = {
...standardOngeki,
image: '/help-standard-ongeki.png',
};
standardChunithm = {
...standardOngeki,
image: '/help-standard-chunithm.png',
};
finaleOngeki = {
...finaleOngeki,
image: '/help-finale-ongeki.png',
};
finaleChunithm = {
...finaleOngeki,
image: '/help-finale-chunithm.png',
};
});
</script>
<template>
<Dialog
modal
:visible="visible"
:closable="false"
:header="
firstTime
? `It looks like you're running ${game ? prettyPrint(game) : '<game>'} for the first time`
: `${game ? prettyPrint(game) : '<game>'} help`
"
:style="{ width: '760px', scale: client.scaleValue }"
>
<Carousel :value="data" :num-visible="1" :num-scroll="1">
<template #item="slotProps">
<div class="md-container markdown">
<vue-markdown-it
:source="processText(slotProps.data?.text)"
:options="{
typographer: true,
breaks: true,
html: true,
}"
/>
</div>
<div
class="border border-surface-200 dark:border-surface-700 rounded m-2"
>
<img :src="slotProps.data.image" />
</div>
</template>
</Carousel>
<div style="width: 100%; text-align: center">
<Button
class="m-auto"
label="OK"
@click="() => onFinish && onFinish()"
/>
</div>
</Dialog>
</template>
<style lang="css">
.p-dialog ::-webkit-scrollbar {
display: none;
}
.md-container {
height: 9.5rem;
}
</style>

View File

@ -5,10 +5,12 @@ import ContextMenu from 'primevue/contextmenu';
import { useConfirm } from 'primevue/useconfirm';
import { listen } from '@tauri-apps/api/event';
import { getCurrentWindow } from '@tauri-apps/api/window';
import Onboarding from './Onboarding.vue';
import { invoke } from '../invoke';
import { usePrfStore } from '../stores';
import { useClientStore, usePrfStore } from '../stores';
const prf = usePrfStore();
const client = useClientStore();
const confirmDialog = useConfirm();
type StartStatus = 'ready' | 'preparing' | 'running';
@ -98,6 +100,7 @@ const menuItems = [
{
label: 'Refresh and start',
icon: 'pi pi-sync',
tooltip: 'test',
command: async () => await startline(false, true),
},
{
@ -110,6 +113,14 @@ const menuItems = [
icon: 'pi pi-link',
command: createShortcut,
},
{
label: 'Help',
icon: 'pi pi-question-circle',
command: () => {
onboardingFirstTime.value = false;
onboardingVisible.value = true;
},
},
];
const menu = ref();
@ -117,9 +128,38 @@ const showContextMenu = (event: Event) => {
event.preventDefault();
menu.value.show(event);
};
const onboardingVisible = ref(false);
const onboardingFirstTime = ref(false);
const tryStart = () => {
const game = prf.current?.meta.game;
if (game !== undefined) {
if (client.onboarded.includes(game)) {
startline(false, false);
} else {
onboardingVisible.value = true;
onboardingFirstTime.value = true;
client.setOnboarded(game);
}
}
};
</script>
<template>
<Onboarding
:visible="onboardingVisible"
:first-time="onboardingFirstTime"
:on-finish="
() => {
onboardingVisible = false;
if (onboardingFirstTime === true) {
startline(false, false);
}
}
"
/>
<ContextMenu ref="menu" :model="menuItems" />
<Button
v-if="startStatus === 'ready'"
@ -130,7 +170,7 @@ const showContextMenu = (event: Event) => {
aria-label="start"
size="small"
class="m-2.5"
@click="startline(false, false)"
@click="tryStart"
@contextmenu="showContextMenu"
/>
<Button

119
src/keyboard.ts Normal file
View File

@ -0,0 +1,119 @@
const KEY_MAP: { [key: number]: string } = {
1: 'M1',
2: 'M2',
4: 'M3',
5: 'M4',
6: 'M5',
8: 'Backspace',
9: 'Tab',
12: 'Clear',
13: 'Enter',
19: 'Pause',
20: 'CapsLock',
27: 'Escape',
32: 'Space',
33: 'PageUp',
34: 'PageDown',
35: 'End',
36: 'Home',
37: 'ArrowLeft',
38: 'ArrowUp',
39: 'ArrowRight',
40: 'ArrowDown',
45: 'Insert',
46: 'Delete',
48: 'Digit0',
49: 'Digit1',
50: 'Digit2',
51: 'Digit3',
52: 'Digit4',
53: 'Digit5',
54: 'Digit6',
55: 'Digit7',
56: 'Digit8',
57: 'Digit9',
65: 'KeyA',
66: 'KeyB',
67: 'KeyC',
68: 'KeyD',
69: 'KeyE',
70: 'KeyF',
71: 'KeyG',
72: 'KeyH',
73: 'KeyI',
74: 'KeyJ',
75: 'KeyK',
76: 'KeyL',
77: 'KeyM',
78: 'KeyN',
79: 'KeyO',
80: 'KeyP',
81: 'KeyQ',
82: 'KeyR',
83: 'KeyS',
84: 'KeyT',
85: 'KeyU',
86: 'KeyV',
87: 'KeyW',
88: 'KeyX',
89: 'KeyY',
90: 'KeyZ',
91: 'MetaLeft',
92: 'MetaRight',
93: 'ContextMenu',
96: 'Numpad0',
97: 'Numpad1',
98: 'Numpad2',
99: 'Numpad3',
100: 'Numpad4',
101: 'Numpad5',
102: 'Numpad6',
103: 'Numpad7',
104: 'Numpad8',
105: 'Numpad9',
106: 'NumpadMultiply',
107: 'NumpadAdd',
109: 'NumpadSubtract',
110: 'NumpadDecimal',
111: 'NumpadDivide',
112: 'F1',
113: 'F2',
114: 'F3',
115: 'F4',
116: 'F5',
117: 'F6',
118: 'F7',
119: 'F8',
120: 'F9',
121: 'F10',
122: 'F11',
123: 'F12',
144: 'NumLock',
145: 'ScrollLock',
160: 'ShiftLeft',
161: 'ShiftRight',
162: 'ControlLeft',
163: 'ControlRight',
164: 'AltLeft',
165: 'AltRight',
186: 'Semicolon',
187: 'Equal',
188: 'Comma',
189: 'Minus',
190: 'Period',
191: 'Slash',
192: 'Backquote',
219: 'BracketLeft',
220: 'Backslash',
221: 'BracketRight',
222: 'Quote',
};
export const fromKeycode = (keyCode: number): string | null => {
return KEY_MAP[keyCode] ?? null;
};
export const toKeycode = (key: string): number | null => {
const res = Object.entries(KEY_MAP).find(([_, v]) => v === key)?.[0];
return res ? parseInt(res) : null;
};

View File

@ -357,6 +357,10 @@ export const usePrfStore = defineStore('prf', () => {
};
});
export enum ClientData {
Onboarded,
}
export const useClientStore = defineStore('client', () => {
type ScaleType = 's' | 'm' | 'l' | 'xl';
const scaleFactor: Ref<ScaleType> = ref('s');
@ -366,16 +370,21 @@ export const useClientStore = defineStore('client', () => {
const enableAutoupdates = ref(true);
const verbose = ref(false);
const theme: Ref<'light' | 'dark' | 'system'> = ref('system');
const onboarded: Ref<Game[]> = ref([]);
const scaleValue = (value: ScaleType) =>
const _scaleValue = (value: ScaleType) =>
value === 's' ? 1 : value === 'm' ? 1.25 : value === 'l' ? 1.5 : 2;
const scaleValue = computed(() => {
return _scaleValue(scaleFactor.value);
});
const setScaleFactor = async (value: ScaleType) => {
scaleFactor.value = value;
const window = getCurrentWindow();
const w = Math.floor(scaleValue(value) * 900);
const h = Math.floor(scaleValue(value) * 480);
const w = Math.floor(_scaleValue(value) * 900);
const h = Math.floor(_scaleValue(value) * 600);
let size = await window.innerSize();
@ -420,6 +429,10 @@ export const useClientStore = defineStore('client', () => {
if (input.theme) {
theme.value = input.theme;
}
if (input.onboarded) {
onboarded.value = input.onboarded;
}
await setTheme(theme.value);
} catch (e) {
console.error(`Error reading client options: ${e}`);
@ -452,6 +465,7 @@ export const useClientStore = defineStore('client', () => {
h: Math.floor(size.height),
},
theme: theme.value,
onboarded: onboarded.value,
})
);
};
@ -499,6 +513,11 @@ export const useClientStore = defineStore('client', () => {
await save();
};
const setOnboarded = async (game: Game) => {
onboarded.value = [...onboarded.value, game];
await save();
};
getCurrentWindow().onResized(async ({ payload }) => {
// For whatever reason this is 0 when minimized
if (payload.width > 0) {
@ -512,8 +531,11 @@ export const useClientStore = defineStore('client', () => {
enableAutoupdates,
verbose,
theme,
onboarded,
timeout,
scaleModel,
_scaleValue,
scaleValue,
load,
save,
queueSave,
@ -521,5 +543,6 @@ export const useClientStore = defineStore('client', () => {
setAutoupdates,
setVerbose,
setTheme,
setOnboarded,
};
});

View File

@ -63,3 +63,12 @@ export const messageSplit = (message: any) => {
export const shouldPreferDark = () => {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
};
export const prettyPrint = (game: Game) => {
switch (game) {
case 'ongeki':
return 'O.N.G.E.K.I.';
case 'chunithm':
return 'CHUNITHM';
}
};

View File

@ -1,6 +1,6 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import tailwindcss from '@tailwindcss/vite';
import { defineConfig } from 'vite';
// @ts-expect-error process is a nodejs global
const host = process.env.TAURI_DEV_HOST;
@ -30,4 +30,7 @@ export default defineConfig(async () => ({
ignored: ['**/rust/**'],
},
},
build: {
chunkSizeWarningLimit: 1024,
},
}));