Compare commits
11 Commits
Author | SHA1 | Date | |
---|---|---|---|
aaeed669df | |||
7084f40404 | |||
f7e9d7d7db | |||
e87b661f08 | |||
5d2d407659 | |||
795e889bd0 | |||
7071f19877 | |||
a72ec25088 | |||
5893536daa | |||
e9550e8eee | |||
658a69a1e2 |
23
CHANGELOG.md
@ -1,3 +1,26 @@
|
||||
## 0.11.1
|
||||
|
||||
- Improved help pages
|
||||
|
||||
## 0.11.0
|
||||
|
||||
- Added help pages
|
||||
|
||||
## 0.10.1
|
||||
|
||||
- Fixed the order of cells in the CHUNITHM keyboard
|
||||
- Fixed numpad bindings with numlock disabled
|
||||
- Disabled primary monitor cleanup when "don't switch primary monitor" is enabled
|
||||
|
||||
## 0.10.0
|
||||
|
||||
- Added a global progress bar
|
||||
- Fixed issues with downloading under certain conditions
|
||||
|
||||
## 0.9.0
|
||||
|
||||
- Added a light/dark theme switcher
|
||||
|
||||
## 0.8.1
|
||||
|
||||
- Hotfixed the program failing to launch if the data dir hadn't already been created
|
||||
|
20
README.md
@ -1,17 +1,19 @@
|
||||
# STARTLINER
|
||||
|
||||
A simple and easy to use launcher, configuration tool and mod manager
|
||||
for O.N.G.E.K.I. and CHUNITHM, using [Rainycolor Watercolor](https://rainy.patafour.zip).
|
||||
This is a program that seeks to streamline game data configuration, currently supporting O.N.G.E.K.I. and CHUNITHM.
|
||||
|
||||
STARTLINER is four things:
|
||||
|
||||
- a mod installer and updater, powered by [Rainycolor Watercolor](https://rainy.patafour.zip),
|
||||
- a configuration GUI for segatools,
|
||||
- a glorified `start.bat` clicker, with automatic monitor setup and rollback,
|
||||
- [an abstraction allowing data configuration without touching the game directory](https://gitea.tendokyu.moe/akanyan/STARTLINER/wiki/Architecture-details).
|
||||
|
||||
STARTLINER's core design principle is to modify, configure and launch games without tampering with them.
|
||||
This makes it possible to keep data cleaner than ever, and to have several configurations pointing at the same data.
|
||||
|
||||
Made with Rust (Tauri) and Vue. Technically multiplatform. Contributions welcome.
|
||||
|
||||
## Features
|
||||
|
||||
- [Clean](https://gitea.tendokyu.moe/akanyan/STARTLINER/wiki/Architecture-details) data modding
|
||||
- Segatools configuration
|
||||
- Monitor configuration with automatic rollback
|
||||
- Support for multiple configurations pointing at the same data
|
||||
|
||||
## Usage
|
||||
|
||||
Download a prebuilt binary from [Releases](https://gitea.tendokyu.moe/akanyan/STARTLINER/releases) or build it yourself:
|
||||
|
1
TODO.md
@ -4,6 +4,5 @@
|
||||
|
||||
### Long-term
|
||||
|
||||
- Progress bars and other GUI sugar
|
||||
- artemis as a special package
|
||||
- Other arcade games (if there is demand)
|
||||
|
3
public/help-chunithm-server.md
Normal 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>
|
BIN
public/help-chunithm-server.png
Normal file
After Width: | Height: | Size: 73 KiB |
BIN
public/help-finale-chunithm.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
public/help-finale-ongeki.png
Normal file
After Width: | Height: | Size: 12 KiB |
8
public/help-finale.md
Normal 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
|
3
public/help-ongeki-lever.md
Normal 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>).
|
BIN
public/help-ongeki-lever.png
Normal file
After Width: | Height: | Size: 90 KiB |
3
public/help-ongeki-system-processing.md
Normal 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.
|
BIN
public/help-ongeki-system-processing.png
Normal file
After Width: | Height: | Size: 54 KiB |
BIN
public/help-standard-chunithm.png
Normal file
After Width: | Height: | Size: 83 KiB |
BIN
public/help-standard-ongeki.png
Normal file
After Width: | Height: | Size: 129 KiB |
7
public/help-standard.md
Normal 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%.
|
@ -1,5 +1,4 @@
|
||||
use std::{collections::HashSet, path::PathBuf};
|
||||
use futures::Stream;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use tokio::fs::File;
|
||||
@ -15,7 +14,7 @@ pub struct DownloadHandler {
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct DownloadTick {
|
||||
pkg_key: PkgKey,
|
||||
ratio: f32
|
||||
ratio: f32,
|
||||
}
|
||||
|
||||
impl DownloadHandler {
|
||||
@ -50,14 +49,15 @@ impl DownloadHandler {
|
||||
|
||||
let mut cache_file_w = File::create(&zip_path_part).await?;
|
||||
let mut byte_stream = reqwest::get(&rmt.download_url).await?.bytes_stream();
|
||||
let first_hint = byte_stream.size_hint().0 as f32;
|
||||
let mut total_bytes = 0;
|
||||
|
||||
log::info!("downloading: {}", rmt.download_url);
|
||||
while let Some(item) = byte_stream.next().await {
|
||||
let i = item?;
|
||||
app.emit("download-tick", DownloadTick {
|
||||
total_bytes += i.len();
|
||||
_ = app.emit("download-progress", DownloadTick {
|
||||
pkg_key: pkg_key.clone(),
|
||||
ratio: 1.0f32 - (byte_stream.size_hint().0 as f32) / first_hint
|
||||
ratio: (total_bytes as f32) / (rmt.file_size as f32),
|
||||
})?;
|
||||
cache_file_w.write_all(&mut i.as_ref()).await?;
|
||||
}
|
||||
|
@ -102,14 +102,15 @@ pub async fn run(_args: Vec<String>) {
|
||||
});
|
||||
|
||||
app.listen("download-end", closure!(clone apph, |ev| {
|
||||
log::debug!("download-end triggered: {}", ev.payload());
|
||||
let raw = ev.payload();
|
||||
log::debug!("download-end triggered: {}", raw);
|
||||
let key = PkgKey(raw[1..raw.len()-1].to_owned());
|
||||
let apph = apph.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let mutex = apph.state::<Mutex<AppData>>();
|
||||
let mut appd = mutex.lock().await;
|
||||
log::debug!("download-end install {:?}", appd.pkgs.install_package(&key, true, false).await);
|
||||
let res = appd.pkgs.install_package(&key, true, false).await;
|
||||
log::debug!("download-end install {:?}", res);
|
||||
});
|
||||
}));
|
||||
|
||||
@ -126,19 +127,21 @@ pub async fn run(_args: Vec<String>) {
|
||||
}));
|
||||
|
||||
app.listen("install-end-prelude", closure!(clone apph, |ev| {
|
||||
log::debug!("install-end-prelude triggered: {}", ev.payload());
|
||||
let payload = serde_json::from_str::<Payload>(ev.payload());
|
||||
log::debug!("install-end-prelude triggered: {:?}", payload);
|
||||
let apph = apph.clone();
|
||||
if let Ok(payload) = payload {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let mutex = apph.state::<Mutex<AppData>>();
|
||||
let mut appd = mutex.lock().await;
|
||||
let res = appd.toggle_package(payload.pkg.clone(), ToggleAction::EnableSelf);
|
||||
log::debug!(
|
||||
"install-end-prelude toggle {:?}",
|
||||
appd.toggle_package(payload.pkg.clone(), ToggleAction::EnableSelf)
|
||||
res
|
||||
);
|
||||
use tauri::Emitter;
|
||||
log::debug!("install-end {:?}", apph.emit("install-end", payload));
|
||||
let res = apph.emit("install-end", payload);
|
||||
log::debug!("install-end {:?}", res);
|
||||
});
|
||||
} else {
|
||||
log::error!("install-end-prelude: invalid payload: {}", ev.payload());
|
||||
@ -232,7 +235,8 @@ pub async fn run(_args: Vec<String>) {
|
||||
let mutex = app.state::<Mutex<AppData>>();
|
||||
let appd = mutex.lock().await;
|
||||
if let Some(p) = &appd.profile {
|
||||
log::debug!("save: {:?}", p.save());
|
||||
let res = p.save();
|
||||
log::debug!("save: {:?}", res);
|
||||
app.exit(0);
|
||||
}
|
||||
});
|
||||
@ -330,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(())
|
||||
|
@ -22,4 +22,5 @@ pub struct V1Version {
|
||||
pub icon: String,
|
||||
pub dependencies: BTreeSet<PkgKeyVersion>,
|
||||
pub download_url: String,
|
||||
pub file_size: i64,
|
||||
}
|
@ -6,14 +6,14 @@ use tauri::{AppHandle, Listener};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DisplayInfo {
|
||||
pub primary: String,
|
||||
pub primary: Option<String>,
|
||||
pub set: Option<DisplaySet>,
|
||||
}
|
||||
|
||||
impl Default for DisplayInfo {
|
||||
fn default() -> Self {
|
||||
DisplayInfo {
|
||||
primary: "default".to_owned(),
|
||||
primary: None,
|
||||
set: query_displays().ok(),
|
||||
}
|
||||
}
|
||||
@ -60,7 +60,7 @@ impl Display {
|
||||
.ok_or_else(|| anyhow!("Unable to query display settings"))?;
|
||||
|
||||
let res = DisplayInfo {
|
||||
primary: primary.name().to_owned(),
|
||||
primary: if self.dont_switch_primary { None } else { Some(primary.name().to_owned()) },
|
||||
set: Some(display_set.clone()),
|
||||
};
|
||||
|
||||
@ -132,12 +132,14 @@ impl Display {
|
||||
let display_set = info.set.as_ref()
|
||||
.ok_or_else(|| anyhow!("Unable to clean up displays: no display set"))?;
|
||||
|
||||
let primary = display_set
|
||||
.displays()
|
||||
.find(|display| display.name() == info.primary)
|
||||
.ok_or_else(|| anyhow!("Display {} not found", info.primary))?;
|
||||
if let Some(info_primary) = &info.primary {
|
||||
let primary = display_set
|
||||
.displays()
|
||||
.find(|display| display.name() == info_primary)
|
||||
.ok_or_else(|| anyhow!("Display {} not found", info_primary))?;
|
||||
|
||||
primary.set_primary()?;
|
||||
primary.set_primary()?;
|
||||
}
|
||||
|
||||
display_set.apply()?;
|
||||
displayz::refresh()?;
|
||||
|
@ -81,6 +81,7 @@ pub struct Remote {
|
||||
pub nsfw: bool,
|
||||
pub categories: Vec<String>,
|
||||
pub dependencies: BTreeSet<PkgKey>,
|
||||
pub file_size: i64,
|
||||
}
|
||||
|
||||
impl PkgKey {
|
||||
@ -112,7 +113,8 @@ impl Package {
|
||||
nsfw: p.has_nsfw_content,
|
||||
version: v.version_number,
|
||||
categories: p.categories,
|
||||
dependencies: Self::sanitize_deps(v.dependencies)
|
||||
dependencies: Self::sanitize_deps(v.dependencies),
|
||||
file_size: v.file_size
|
||||
}),
|
||||
source: PackageSource::Rainy,
|
||||
})
|
||||
|
@ -21,7 +21,7 @@ pub struct PackageStore {
|
||||
offline: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct Payload {
|
||||
pub pkg: PkgKey
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "STARTLINER",
|
||||
"version": "0.8.1",
|
||||
"version": "0.11.1",
|
||||
"identifier": "zip.patafour.startliner",
|
||||
"build": {
|
||||
"beforeDevCommand": "bun run dev",
|
||||
|
@ -28,7 +28,9 @@ import {
|
||||
usePrfStore,
|
||||
} from '../stores';
|
||||
import { Dirs } from '../types';
|
||||
import { messageSplit } from '../util';
|
||||
import { messageSplit, shouldPreferDark } from '../util';
|
||||
|
||||
document.documentElement.classList.toggle('use-dark-mode', shouldPreferDark());
|
||||
|
||||
const pkg = usePkgStore();
|
||||
const prf = usePrfStore();
|
||||
@ -86,6 +88,60 @@ listen<string>('launch-error', (event) => {
|
||||
errorMessage.value = event.payload;
|
||||
errorHeader.value = 'Launch error';
|
||||
});
|
||||
|
||||
interface DownloadingStatus {
|
||||
ratio: number;
|
||||
pkg_key: string;
|
||||
}
|
||||
const downloading_status: Ref<DownloadingStatus[]> = ref([]);
|
||||
|
||||
const download_value = computed(() => {
|
||||
return (
|
||||
downloading_status.value.map((v) => v.ratio).reduce((a, v) => a * v) *
|
||||
100
|
||||
);
|
||||
});
|
||||
|
||||
const downloadProgressText = computed(() => {
|
||||
if (download_value.value < 7) {
|
||||
return '';
|
||||
}
|
||||
let pkgs = `${downloading_status.value.length} package${downloading_status.value.length === 1 ? '' : 's'}`;
|
||||
if (download_value.value < 14) {
|
||||
return pkgs;
|
||||
} else {
|
||||
return `${pkgs} (${Math.floor(download_value.value)}%)`;
|
||||
}
|
||||
});
|
||||
|
||||
listen<DownloadingStatus>('download-progress', (event) => {
|
||||
let status = downloading_status.value.find(
|
||||
(v) => v.pkg_key === event.payload.pkg_key
|
||||
);
|
||||
|
||||
if (status === undefined) {
|
||||
status = {
|
||||
ratio: 0,
|
||||
pkg_key: event.payload.pkg_key,
|
||||
};
|
||||
downloading_status.value.push(status);
|
||||
}
|
||||
status.ratio = event.payload.ratio;
|
||||
|
||||
const remove = () => {
|
||||
if (status !== undefined) {
|
||||
downloading_status.value = downloading_status.value.filter(
|
||||
(v) => v.pkg_key !== event.payload.pkg_key
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (status.ratio === 1.0) {
|
||||
remove();
|
||||
}
|
||||
|
||||
setTimeout(() => remove, 10_000);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -100,6 +156,16 @@ listen<string>('launch-error', (event) => {
|
||||
: 'main-scale-xl'
|
||||
"
|
||||
>
|
||||
<div
|
||||
v-if="downloading_status.length > 0"
|
||||
class="download-progress-bg"
|
||||
></div>
|
||||
<ProgressBar
|
||||
v-if="downloading_status.length > 0"
|
||||
:value="download_value"
|
||||
class="download-progress"
|
||||
>{{ downloadProgressText }}</ProgressBar
|
||||
>
|
||||
<ConfirmDialog>
|
||||
<template #message="{ message }">
|
||||
<ScrollPanel
|
||||
@ -351,4 +417,23 @@ body {
|
||||
.p-progressbar-label {
|
||||
transition-duration: 0s !important;
|
||||
}
|
||||
|
||||
.download-progress {
|
||||
position: fixed !important;
|
||||
bottom: 0;
|
||||
left: 5vw;
|
||||
width: 90vw;
|
||||
z-index: 10000 !important;
|
||||
margin: 20px auto;
|
||||
}
|
||||
.download-progress-bg {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 60px;
|
||||
background-color: var(--p-surface-900);
|
||||
border-top: 1px solid var(--p-surface-600);
|
||||
z-index: 998;
|
||||
}
|
||||
</style>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import Button from 'primevue/button';
|
||||
import { invoke } from '../invoke';
|
||||
import { usePkgStore } from '../stores';
|
||||
@ -11,20 +12,26 @@ const props = defineProps({
|
||||
pkg: Object as () => Package,
|
||||
});
|
||||
|
||||
const deleting = ref(false);
|
||||
|
||||
const remove = async () => {
|
||||
if (props.pkg === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
deleting.value = true;
|
||||
|
||||
await invoke('delete_package', {
|
||||
key: pkgKey(props.pkg),
|
||||
});
|
||||
|
||||
deleting.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Button
|
||||
v-if="pkg?.loc && !pkg?.js.busy"
|
||||
v-if="pkg?.loc && !pkg?.js.downloading"
|
||||
rounded
|
||||
icon="pi pi-trash"
|
||||
severity="danger"
|
||||
@ -32,7 +39,7 @@ const remove = async () => {
|
||||
size="small"
|
||||
class="self-center ml-4"
|
||||
style="width: 2rem; height: 2rem"
|
||||
:loading="pkg?.js.busy"
|
||||
:loading="deleting"
|
||||
v-on:click="remove()"
|
||||
/>
|
||||
|
||||
@ -45,7 +52,7 @@ const remove = async () => {
|
||||
size="small"
|
||||
class="self-center ml-4"
|
||||
style="width: 2rem; height: 2rem"
|
||||
:loading="pkg?.js.busy"
|
||||
:loading="pkg?.js.downloading"
|
||||
v-on:click="async () => await pkgs.install(pkg)"
|
||||
/>
|
||||
</template>
|
||||
|
@ -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';
|
||||
|
||||
@ -15,9 +16,51 @@ const handleKey = (
|
||||
) => {
|
||||
event.preventDefault();
|
||||
|
||||
const keycode = toKeycode(event.code);
|
||||
let keycode = toKeycode(event.code);
|
||||
|
||||
if (keycode !== null && button !== undefined) {
|
||||
const data = prf.current!.data.keyboard!.data as any;
|
||||
|
||||
if (event.getModifierState('NumLock') === false) {
|
||||
switch (event.code) {
|
||||
case 'NumpadDecimal':
|
||||
keycode = toKeycode('Delete');
|
||||
break;
|
||||
case 'Numpad0':
|
||||
keycode = toKeycode('Insert');
|
||||
break;
|
||||
case 'Numpad1':
|
||||
keycode = toKeycode('End');
|
||||
break;
|
||||
case 'Numpad2':
|
||||
keycode = toKeycode('ArrowDown');
|
||||
break;
|
||||
case 'Numpad3':
|
||||
keycode = toKeycode('PageDown');
|
||||
break;
|
||||
case 'Numpad4':
|
||||
keycode = toKeycode('ArrowLeft');
|
||||
break;
|
||||
case 'Numpad5':
|
||||
keycode = toKeycode('Clear');
|
||||
break;
|
||||
case 'Numpad6':
|
||||
keycode = toKeycode('ArrowRight');
|
||||
break;
|
||||
case 'Numpad7':
|
||||
keycode = toKeycode('Home');
|
||||
break;
|
||||
case 'Numpad8':
|
||||
keycode = toKeycode('ArrowUp');
|
||||
break;
|
||||
case 'Numpad9':
|
||||
keycode = toKeycode('PageUp');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (index !== undefined) {
|
||||
data[button][index] = keycode;
|
||||
} else {
|
||||
@ -75,7 +118,7 @@ const handleMouse = (
|
||||
}
|
||||
};
|
||||
|
||||
const getKey = (key: keyof OngekiButtons, index?: number) =>
|
||||
const getKey = (key: keyof OngekiButtons, index?: number): any =>
|
||||
computed(() => {
|
||||
const data = prf.current!.data.keyboard?.data as any;
|
||||
const keycode =
|
||||
@ -85,147 +128,45 @@ const getKey = (key: keyof OngekiButtons, index?: number) =>
|
||||
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',
|
||||
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;
|
||||
};
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
small: Boolean,
|
||||
verySmall: Boolean,
|
||||
tall: Boolean,
|
||||
tooltip: String,
|
||||
button: String,
|
||||
color: String,
|
||||
index: Number,
|
||||
});
|
||||
|
||||
const modelValue = computed(() => {
|
||||
return getKey(props.button as keyof OngekiButtons, props.index).value;
|
||||
});
|
||||
|
||||
const fontSize = computed(() => {
|
||||
if (!props.small) {
|
||||
return '1rem';
|
||||
}
|
||||
const len = modelValue.value.length;
|
||||
if (len < 5) {
|
||||
return '1rem';
|
||||
}
|
||||
if (len < 7) {
|
||||
return '0.75rem';
|
||||
}
|
||||
return '0.5rem';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<InputText
|
||||
:style="{
|
||||
width: small ? '3em' : '5em',
|
||||
height: small ? '3em' : tall ? '10em' : '5em',
|
||||
fontSize: small ? '0.9em' : '1em',
|
||||
width: small ? '2.8rem' : '5rem',
|
||||
height: small ? '2.8rem' : tall ? '10rem' : '5rem',
|
||||
fontSize,
|
||||
backgroundColor: color,
|
||||
}"
|
||||
unstyled
|
||||
class="text-center buttoninputtext"
|
||||
v-tooltip="tooltip"
|
||||
v-tooltip="tooltip ? `${tooltip}: ${modelValue}` : undefined"
|
||||
@contextmenu.prevent="() => {}"
|
||||
@keydown="(ev: KeyboardEvent) => handleKey(button, ev, index)"
|
||||
@mousedown="
|
||||
@ -233,7 +174,7 @@ defineProps({
|
||||
handleMouse(button as keyof OngekiButtons, ev, index)
|
||||
"
|
||||
@focusout="() => (hasClickedM1Once = false)"
|
||||
:model-value="getKey(button as keyof OngekiButtons, index) as any"
|
||||
:model-value="modelValue"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@ -241,5 +182,7 @@ defineProps({
|
||||
.buttoninputtext {
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(200, 200, 200, 0.3);
|
||||
overflow: scroll !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
</style>
|
||||
|
@ -15,7 +15,6 @@ const props = defineProps({
|
||||
|
||||
const pkgs = usePkgStore();
|
||||
const prf = usePrfStore();
|
||||
const groupCallIndex = ref(0);
|
||||
const empty = ref(false);
|
||||
const gameSublist: Ref<string[]> = ref([]);
|
||||
|
||||
@ -46,10 +45,7 @@ const group = computed(() => {
|
||||
({ namespace }) => namespace
|
||||
)
|
||||
);
|
||||
if (groupCallIndex.value > 0) {
|
||||
empty.value = Object.keys(res).length === 0;
|
||||
}
|
||||
groupCallIndex.value += 1;
|
||||
empty.value = Object.keys(res).length === 0;
|
||||
return res;
|
||||
});
|
||||
|
||||
|
@ -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"
|
||||
|
175
src/components/Onboarding.vue
Normal file
@ -0,0 +1,175 @@
|
||||
<script setup lang="ts">
|
||||
import { ComputedRef, computed, onMounted, ref } 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();
|
||||
|
||||
const props = 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',
|
||||
};
|
||||
});
|
||||
|
||||
const counter = ref(0);
|
||||
|
||||
const exitLabel = computed(() => {
|
||||
return props.firstTime === true && counter.value < data.value.length - 1
|
||||
? 'Skip'
|
||||
: 'Close';
|
||||
});
|
||||
</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"
|
||||
:page="counter"
|
||||
v-on:update:page="(p) => (counter = p)"
|
||||
>
|
||||
<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
|
||||
v-if="counter < data.length - 1"
|
||||
class="m-auto mr-4"
|
||||
label="Next"
|
||||
@click="() => (counter += 1)"
|
||||
/>
|
||||
<Button
|
||||
class="m-auto"
|
||||
:label="exitLabel"
|
||||
@click="() => onFinish && onFinish()"
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<style lang="css">
|
||||
.p-dialog ::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.md-container {
|
||||
height: 9.5rem;
|
||||
}
|
||||
</style>
|
@ -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
|
||||
|
@ -20,17 +20,15 @@ const install = async () => {
|
||||
});
|
||||
} catch (err) {
|
||||
if (props.pkg !== undefined) {
|
||||
props.pkg.js.busy = false;
|
||||
props.pkg.js.downloading = false;
|
||||
}
|
||||
}
|
||||
|
||||
//if (rv === 'Deferred') { /* download progress */ }
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Button
|
||||
v-if="needsUpdate(pkg) && !pkg?.js.busy"
|
||||
v-if="needsUpdate(pkg) && !pkg?.js.downloading"
|
||||
rounded
|
||||
icon="pi pi-download"
|
||||
severity="success"
|
||||
|
@ -124,7 +124,7 @@ const prf = usePrfStore();
|
||||
<div
|
||||
v-for="idx in Array(16)
|
||||
.fill(0)
|
||||
.map((_, i) => 16 - i)"
|
||||
.map((_, i) => 32 - 2 * i - 1)"
|
||||
>
|
||||
<KeyboardKey
|
||||
button="cell"
|
||||
@ -142,7 +142,7 @@ const prf = usePrfStore();
|
||||
<div
|
||||
v-for="idx in Array(16)
|
||||
.fill(0)
|
||||
.map((_, i) => 32 - i)"
|
||||
.map((_, i) => 32 - 2 * i)"
|
||||
>
|
||||
<KeyboardKey
|
||||
button="cell"
|
||||
|
@ -34,6 +34,15 @@ const verboseModel = computed({
|
||||
await client.setVerbose(value);
|
||||
},
|
||||
});
|
||||
|
||||
const themeModel = computed({
|
||||
get() {
|
||||
return client.theme;
|
||||
},
|
||||
async set(value: 'light' | 'dark' | 'system') {
|
||||
await client.setTheme(value);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -67,5 +76,18 @@ const verboseModel = computed({
|
||||
>
|
||||
<ToggleSwitch v-model="verboseModel" />
|
||||
</OptionRow>
|
||||
<OptionRow title="Theme">
|
||||
<SelectButton
|
||||
v-model="themeModel"
|
||||
:options="[
|
||||
{ title: 'System', value: 'system' },
|
||||
{ title: 'Light', value: 'light' },
|
||||
{ title: 'Dark', value: 'dark' },
|
||||
]"
|
||||
:allow-empty="false"
|
||||
option-label="title"
|
||||
option-value="value"
|
||||
/>
|
||||
</OptionRow>
|
||||
</OptionCategory>
|
||||
</template>
|
||||
|
119
src/keyboard.ts
Normal 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;
|
||||
};
|
@ -17,6 +17,9 @@ app.use(pinia);
|
||||
app.use(PrimeVue, {
|
||||
theme: {
|
||||
preset: Preset,
|
||||
options: {
|
||||
darkModeSelector: '.use-dark-mode',
|
||||
},
|
||||
},
|
||||
});
|
||||
app.use(ConfirmationService);
|
||||
|
@ -6,7 +6,12 @@ import { PhysicalSize, getCurrentWindow } from '@tauri-apps/api/window';
|
||||
import { readTextFile, writeTextFile } from '@tauri-apps/plugin-fs';
|
||||
import { invoke, invoke_nopopup } from './invoke';
|
||||
import { Dirs, Feature, Game, Package, Profile, ProfileMeta } from './types';
|
||||
import { changePrimaryColor, hasFeature, pkgKey } from './util';
|
||||
import {
|
||||
changePrimaryColor,
|
||||
hasFeature,
|
||||
pkgKey,
|
||||
shouldPreferDark,
|
||||
} from './util';
|
||||
|
||||
type InstallStatus = {
|
||||
pkg: string;
|
||||
@ -114,13 +119,13 @@ export const usePkgStore = defineStore('pkg', {
|
||||
listen<InstallStatus>('install-start', async (ev) => {
|
||||
const key = ev.payload.pkg;
|
||||
await this.reload(key);
|
||||
this.pkg[key].js.busy = true;
|
||||
this.pkg[key].js.downloading = true;
|
||||
});
|
||||
|
||||
listen<InstallStatus>('install-end', async (ev) => {
|
||||
const key = ev.payload.pkg;
|
||||
await this.reload(key);
|
||||
this.pkg[key].js.busy = false;
|
||||
this.pkg[key].js.downloading = false;
|
||||
});
|
||||
},
|
||||
|
||||
@ -147,17 +152,22 @@ export const usePkgStore = defineStore('pkg', {
|
||||
|
||||
async reloadWith(key: string, pkg: Package) {
|
||||
if (this.pkg[key] === undefined) {
|
||||
this.pkg[key] = { js: { busy: false } } as Package;
|
||||
this.pkg[key] = { js: { downloading: false } } as Package;
|
||||
} else {
|
||||
this.pkg[key].loc = null;
|
||||
this.pkg[key].rmt = null;
|
||||
}
|
||||
Object.assign(this.pkg[key], pkg);
|
||||
|
||||
if (!pkg.js) {
|
||||
pkg.js = { downloading: false };
|
||||
}
|
||||
|
||||
if (pkg.rmt !== null) {
|
||||
pkg.rmt.categories.forEach((c) =>
|
||||
this.availableCategories.add(c)
|
||||
);
|
||||
pkg.js.downloading = false;
|
||||
}
|
||||
},
|
||||
|
||||
@ -188,9 +198,8 @@ export const usePkgStore = defineStore('pkg', {
|
||||
force: true,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
if (pkg !== undefined) {
|
||||
pkg.js.busy = false;
|
||||
pkg.js.downloading = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -348,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');
|
||||
@ -356,16 +369,22 @@ export const useClientStore = defineStore('client', () => {
|
||||
const offlineMode = ref(false);
|
||||
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();
|
||||
|
||||
@ -406,6 +425,15 @@ export const useClientStore = defineStore('client', () => {
|
||||
if (input.scaleFactor) {
|
||||
await setScaleFactor(input.scaleFactor);
|
||||
}
|
||||
|
||||
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}`);
|
||||
}
|
||||
@ -436,6 +464,8 @@ export const useClientStore = defineStore('client', () => {
|
||||
w: Math.floor(size.width),
|
||||
h: Math.floor(size.height),
|
||||
},
|
||||
theme: theme.value,
|
||||
onboarded: onboarded.value,
|
||||
})
|
||||
);
|
||||
};
|
||||
@ -468,6 +498,26 @@ export const useClientStore = defineStore('client', () => {
|
||||
await invoke('set_global_config', { field: 'verbose', value });
|
||||
};
|
||||
|
||||
const setTheme = async (value: 'light' | 'dark' | 'system') => {
|
||||
if (value === 'dark') {
|
||||
document.documentElement.classList.add('use-dark-mode');
|
||||
} else if (value === 'light') {
|
||||
document.documentElement.classList.remove('use-dark-mode');
|
||||
} else {
|
||||
document.documentElement.classList.toggle(
|
||||
'use-dark-mode',
|
||||
shouldPreferDark()
|
||||
);
|
||||
}
|
||||
theme.value = value;
|
||||
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) {
|
||||
@ -480,13 +530,19 @@ export const useClientStore = defineStore('client', () => {
|
||||
offlineMode,
|
||||
enableAutoupdates,
|
||||
verbose,
|
||||
theme,
|
||||
onboarded,
|
||||
timeout,
|
||||
scaleModel,
|
||||
_scaleValue,
|
||||
scaleValue,
|
||||
load,
|
||||
save,
|
||||
queueSave,
|
||||
setOfflineMode,
|
||||
setAutoupdates,
|
||||
setVerbose,
|
||||
setTheme,
|
||||
setOnboarded,
|
||||
};
|
||||
});
|
||||
|
@ -19,7 +19,7 @@ export interface Package {
|
||||
icon: string;
|
||||
} | null;
|
||||
js: {
|
||||
busy: boolean;
|
||||
downloading: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
|
13
src/util.ts
@ -59,3 +59,16 @@ export const hasFeature = (pkg: Package | undefined, feature: Feature) => {
|
||||
export const messageSplit = (message: any) => {
|
||||
return message.message?.split('\n');
|
||||
};
|
||||
|
||||
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';
|
||||
}
|
||||
};
|
||||
|
@ -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,
|
||||
},
|
||||
}));
|
||||
|