Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ Thumbs.db

# Local Claude settings
.claude/settings.local.json
test-src/
48 changes: 48 additions & 0 deletions ActionData.json
Original file line number Diff line number Diff line change
Expand Up @@ -562,5 +562,53 @@
}
}
]
},
{
"id": "Fuck_u",
"name": "发Q",
"command": "Q",
"animStateName": "Fuck_u",
"animBegin": "0",
"animEnd": "1",
"animSpeed": 1,
"dirChangeable": false,
"TimelineDatas": [
{
"$type": "MoveStateData, Assembly-CSharp",
"timingBegin": 0,
"timingEnd": 1,
"useGhostLayer": true,
"useGravity": true,
"useCommand": false,
"moveVelMultiAddition": 0,
"useRootMotion": false
},
{
"$type": "JointCollData, Assembly-CSharp",
"timingBegin": 0,
"timingEnd": 1,
"joints": [],
"battleData": {
"damage": 0,
"damageInterval": 1,
"damageDamping": 0,
"criticalRateEx": 0,
"makeBreak": false,
"impartType": 0
}
}
],
"nextActionId": "Fuck_u",
"derivations": [
{
"priority": 0,
"checkPeriod": {
"min": 0,
"max": 1
},
"fastExitTime": 0,
"nextActionId": ""
}
]
}
]
49 changes: 49 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,57 @@
use serde::Serialize;
use std::fs;
use std::path::Path;

#[derive(Serialize)]
struct SchemaSource {
path: String,
contents: String,
}

fn is_schema_file_name(file_name: &str) -> bool {
file_name == "ActionData.cs" || (file_name.ends_with("Data.cs") && !file_name.ends_with(".Designer.cs"))
}

#[tauri::command]
fn load_timeline_schema_sources(schema_directory_path: String) -> Result<Vec<SchemaSource>, String> {
let directory = Path::new(&schema_directory_path);

let mut schema_paths = fs::read_dir(directory)
.map_err(|error| format!("Failed to read schema directory: {error}"))?
.filter_map(|entry| entry.ok())
.filter_map(|entry| {
let file_name = entry.file_name();
let file_name = file_name.to_str()?;
if entry.file_type().ok()?.is_file() && is_schema_file_name(file_name) {
Some(entry.path())
} else {
None
}
})
.collect::<Vec<_>>();

schema_paths.sort();

schema_paths
.into_iter()
.map(|path| {
let contents = fs::read_to_string(&path)
.map_err(|error| format!("Failed to read schema file {}: {error}", path.display()))?;

Ok(SchemaSource {
path: path.to_string_lossy().into_owned(),
contents,
})
})
.collect()
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.invoke_handler(tauri::generate_handler![load_timeline_schema_sources])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
3 changes: 1 addition & 2 deletions src/components/actions/ActionEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { DerivationEditor } from "./DerivationEditor";
import { TimelineEditor } from "@/components/timelines/TimelineEditor";
import type { ActionData, ActionDerivation, NumericValue, TimelinePatch } from "@/models/actionData";
import type { KnownTimelineType } from "@/models/timelineTypes";

const actionFormSchema = z.object({
id: z.string().optional(),
Expand All @@ -32,7 +31,7 @@ interface ActionEditorProps {
highlightedValidationPath: string | null;
onUpdateAction: (patch: Partial<ActionData>) => void;
onSelectTimeline: (id: string) => void;
onAddTimeline: (type: KnownTimelineType) => void;
onAddTimeline: (type: string) => void;
onUpdateTimeline: (timelineId: string, patch: TimelinePatch) => void;
onDuplicateTimeline: (timelineId: string) => void;
onDeleteTimeline: (timelineId: string) => void;
Expand Down
4 changes: 2 additions & 2 deletions src/components/actions/DerivationEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ export const DerivationEditor = ({ derivations, actionIndex, highlightedValidati
<section className="grid gap-3">
<div className="flex items-center justify-between">
<div>
<h2 className="font-mono text-2xl font-black leading-none text-black">Derivations</h2>
<p className="text-sm text-muted-foreground">配置连招派生窗口与下一个动作。</p>
<h2 className="font-mono text-2xl font-black leading-none text-white drop-shadow">Derivations</h2>
<p className="text-sm text-white/80 drop-shadow">配置连招派生窗口与下一个动作。</p>
</div>
<Button
type="button"
Expand Down
10 changes: 8 additions & 2 deletions src/components/fields/SelectField.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import { Select } from "@/components/ui/select";
import { cn } from "@/lib/utils";

interface SelectFieldProps {
label: string;
value: string | number | undefined;
onChange: (value: string) => void;
options: Array<{ value: string | number; label: string }>;
highlighted?: boolean;
validationPath?: string;
}

export const SelectField = ({ label, value, onChange, options }: SelectFieldProps) => (
<label className="grid gap-1.5">
export const SelectField = ({ label, value, onChange, options, highlighted = false, validationPath }: SelectFieldProps) => (
<label
data-validation-path={validationPath}
className={cn("grid gap-1.5 rounded-md transition", highlighted && "bg-destructive/10 p-2 ring-2 ring-destructive")}
>
<span className="text-[11px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">{label}</span>
<Select value={value ?? ""} onChange={(event) => onChange(event.target.value)}>
{options.map((option) => (
Expand Down
104 changes: 104 additions & 0 deletions src/components/fields/StringArrayField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { useEffect, useState } from "react";
import { X } from "lucide-react";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";

interface StringArrayFieldProps {
label: string;
value: string[] | undefined;
onChange: (value: string[]) => void;
placeholder?: string;
highlighted?: boolean;
validationPath?: string;
}

const splitPattern = /[\s,]+/;

const normalizeTokens = (value: string) =>
value
.split(splitPattern)
.map((item) => item.trim())
.filter(Boolean);

export const StringArrayField = ({
label,
value,
onChange,
placeholder,
highlighted = false,
validationPath,
}: StringArrayFieldProps) => {
const [draft, setDraft] = useState("");
const [isComposing, setIsComposing] = useState(false);
const items = value ?? [];

useEffect(() => {
setDraft("");
}, [items.join("\u0000")]);

const commitDraft = (rawValue: string) => {
const nextItems = normalizeTokens(rawValue);
if (nextItems.length === 0) {
setDraft("");
return;
}

onChange([...items, ...nextItems]);
setDraft("");
};

return (
<label
data-validation-path={validationPath}
className={cn("grid gap-1.5 rounded-md transition", highlighted && "bg-destructive/10 p-2 ring-2 ring-destructive")}
>
<span className="text-[11px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">{label}</span>
<div className="flex min-h-[3.5rem] flex-wrap items-center gap-2 rounded-[5px] border border-input bg-white px-3 py-2 transition focus-within:border-black focus-within:ring-2 focus-within:ring-black/10">
{items.map((item, index) => (
<span key={`${item}-${index}`} className="inline-flex items-center gap-2 rounded-full bg-[#d8ecff] px-3 py-1 text-sm font-medium text-[#0a63c9]">
{item}
<button
type="button"
className="inline-flex h-4 w-4 items-center justify-center rounded-full text-[#0a63c9] transition hover:bg-[#b9dbff]"
onClick={() => onChange(items.filter((_, itemIndex) => itemIndex !== index))}
>
<X className="h-3 w-3" />
</button>
</span>
))}
<Input
className="h-8 min-w-[12rem] flex-1 border-0 px-0 py-0 shadow-none focus-visible:ring-0"
value={draft}
placeholder={items.length === 0 ? placeholder : undefined}
onChange={(event) => setDraft(event.target.value)}
onBlur={() => commitDraft(draft)}
onCompositionStart={() => setIsComposing(true)}
onCompositionEnd={() => setIsComposing(false)}
onKeyDown={(event) => {
if (event.key === "Backspace" && draft.length === 0 && items.length > 0) {
onChange(items.slice(0, -1));
return;
}

if (isComposing) {
return;
}

if (event.key === "Enter" || event.key === "," || event.key === " ") {
event.preventDefault();
commitDraft(draft);
}
}}
onPaste={(event) => {
const pasted = event.clipboardData.getData("text");
if (!splitPattern.test(pasted)) {
return;
}
event.preventDefault();
commitDraft(`${draft} ${pasted}`);
}}
/>
</div>
</label>
);
};
61 changes: 34 additions & 27 deletions src/components/layout/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@ export const AppShell = () => {
};

useEffect(() => {
void store.restoreLastFile();
}, []);
void (async () => {
await store.restoreLastSchemaDirectory();
await store.restoreLastFile();
})();
}, []);

useEffect(() => {
if (!highlightedValidationPath) {
Expand Down Expand Up @@ -67,10 +70,12 @@ export const AppShell = () => {
<div className="flex h-screen flex-col overflow-hidden bg-background text-foreground">
<Toolbar
filePath={store.filePath}
schemaDirectoryPath={store.schemaDirectoryPath}
dirty={store.dirty}
issueCount={store.validationIssues.length}
errorCount={errorCount}
onOpen={store.openFile}
onOpenSchemaDirectory={store.openSchemaDirectory}
onSave={store.saveFile}
onSaveAs={store.saveFileAs}
onValidate={validateAndOpen}
Expand All @@ -90,31 +95,33 @@ export const AppShell = () => {
onDelete={store.deleteAction}
onMove={store.moveAction}
/>
<section
className="relative min-h-0 overflow-auto rounded-lg border border-border p-4 simple-card-shadow"
style={{
backgroundImage: `linear-gradient(135deg, rgba(8, 10, 24, 0.72), rgba(31, 10, 49, 0.58)), url(${editorBackgroundImage})`,
backgroundPosition: "center center",
backgroundRepeat: "no-repeat",
backgroundSize: "cover",
}}
>
<div className="absolute inset-0 bg-white/30 backdrop-blur-[2px]" />
<div className="relative z-10">
<ActionEditor
action={selectedAction}
actionIndex={selectedActionIndex}
selectedTimelineId={store.selectedTimelineId}
highlightedValidationPath={highlightedValidationPath}
onUpdateAction={(patch) => selectedAction && store.updateAction(selectedAction.__editorId, patch)}
onSelectTimeline={store.selectTimeline}
onAddTimeline={(type) => selectedAction && store.addTimeline(selectedAction.__editorId, type)}
onUpdateTimeline={(timelineId, patch) => selectedAction && store.updateTimeline(selectedAction.__editorId, timelineId, patch)}
onDuplicateTimeline={(timelineId) => selectedAction && store.duplicateTimeline(selectedAction.__editorId, timelineId)}
onDeleteTimeline={(timelineId) => selectedAction && store.deleteTimeline(selectedAction.__editorId, timelineId)}
onMoveTimeline={(timelineId, direction) => selectedAction && store.moveTimeline(selectedAction.__editorId, timelineId, direction)}
onUpdateDerivations={(derivations) => selectedAction && store.updateDerivations(selectedAction.__editorId, derivations)}
/>
<section className="min-h-0 overflow-auto rounded-lg border border-border bg-slate-950 simple-card-shadow">
<div
className="relative min-h-full p-4"
style={{
backgroundImage: `linear-gradient(135deg, rgba(8, 10, 24, 0.72), rgba(31, 10, 49, 0.58)), url(${editorBackgroundImage})`,
backgroundPosition: "center top",
backgroundRepeat: "no-repeat",
backgroundSize: "cover",
}}
>
<div className="absolute inset-0 bg-slate-950/20 backdrop-blur-[1px]" />
<div className="relative z-10">
<ActionEditor
action={selectedAction}
actionIndex={selectedActionIndex}
selectedTimelineId={store.selectedTimelineId}
highlightedValidationPath={highlightedValidationPath}
onUpdateAction={(patch) => selectedAction && store.updateAction(selectedAction.__editorId, patch)}
onSelectTimeline={store.selectTimeline}
onAddTimeline={(type) => selectedAction && store.addTimeline(selectedAction.__editorId, type)}
onUpdateTimeline={(timelineId, patch) => selectedAction && store.updateTimeline(selectedAction.__editorId, timelineId, patch)}
onDuplicateTimeline={(timelineId) => selectedAction && store.duplicateTimeline(selectedAction.__editorId, timelineId)}
onDeleteTimeline={(timelineId) => selectedAction && store.deleteTimeline(selectedAction.__editorId, timelineId)}
onMoveTimeline={(timelineId, direction) => selectedAction && store.moveTimeline(selectedAction.__editorId, timelineId, direction)}
onUpdateDerivations={(derivations) => selectedAction && store.updateDerivations(selectedAction.__editorId, derivations)}
/>
</div>
</div>
</section>
</main>
Expand Down
Loading
Loading