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
291 changes: 287 additions & 4 deletions frontend/src-tauri/Cargo.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion frontend/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ tauri-build = { version = "1.5.1", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.5.4", features = ["dialog-all", "fs-all", "path-all", "protocol-asset"] }
tauri = { version = "1.5.4", features = [ "shell-open", "dialog-all", "fs-all", "path-all", "protocol-asset"] }
arboard = "3.3.0"

[features]
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
Expand Down
53 changes: 52 additions & 1 deletion frontend/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
use tauri::{Manager, State};
use std::sync::Mutex;
use std::path::PathBuf;
use arboard::Clipboard;

// State to hold the current project path
struct ProjectState {
Expand Down Expand Up @@ -41,6 +42,54 @@ fn get_project_path(state: State<ProjectState>) -> Result<Option<String>, String
Ok(project_path.clone())
}

#[tauri::command]
fn read_clipboard_text() -> Result<String, String> {
let mut clipboard = Clipboard::new().map_err(|e| e.to_string())?;
clipboard.get_text().map_err(|e| e.to_string())
}

#[tauri::command]
fn open_project_directory(path: String) -> Result<(), String> {
#[cfg(target_os = "windows")]
{
std::process::Command::new("explorer")
.arg(&path)
.spawn()
.map_err(|e| format!("Failed to open directory: {}", e))?;
}

#[cfg(target_os = "macos")]
{
std::process::Command::new("open")
.arg(&path)
.spawn()
.map_err(|e| format!("Failed to open directory: {}", e))?;
}

#[cfg(target_os = "linux")]
{
// Try common file managers
let file_managers = ["xdg-open", "nautilus", "dolphin", "thunar", "pcmanfm"];
let mut opened = false;

for fm in &file_managers {
if let Ok(_) = std::process::Command::new(fm)
.arg(&path)
.spawn()
{
opened = true;
break;
}
}

if !opened {
return Err("Failed to open directory: No suitable file manager found".to_string());
}
}

Ok(())
}

fn main() {
tauri::Builder::default()
.setup(|app| {
Expand All @@ -56,7 +105,9 @@ fn main() {
.invoke_handler(tauri::generate_handler![
get_commands_path,
set_project_path,
get_project_path
get_project_path,
read_clipboard_text,
open_project_directory
]) // Register all commands
.run(tauri::generate_context!())
.expect("error while running tauri application");
Expand Down
3 changes: 3 additions & 0 deletions frontend/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
"protocol": {
"asset": true,
"assetScope": ["**"]
},
"shell": {
"open": true
}
},
"bundle": {
Expand Down
62 changes: 54 additions & 8 deletions frontend/src/components/skit/CommandEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { CommandDefinition, PropertyDefinition } from '../../types';
import { SkitCommand } from '../../types';
import { formatCommandPreview } from '../../utils/commandFormatting';
import { ColorPicker } from '../ui/color-picker';
import { HexColorPicker } from 'react-colorful';
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
import { VectorInput } from '../ui/vector-input';
import { useTranslation } from 'react-i18next';
import { useCommandTranslation } from '../../hooks/useCommandTranslation';
Expand Down Expand Up @@ -98,6 +100,10 @@ export function CommandEditor() {
const isBgMixed = !backgroundColors.every(c => c === backgroundColors[0]);
const bgColorValue = isBgMixed ? "#ffffff" : backgroundColors[0];

const labelColors = selectedCommands.map(cmd => cmd.commandLabelColor || (commandsMap.get(cmd.type)?.defaultCommandLabelColor ?? "#000000"));
const isLabelColorMixed = !labelColors.every(c => c === labelColors[0]);
const labelColorValue = isLabelColorMixed ? "#000000" : labelColors[0];

const selectedLabels = selectedCommands.map(cmd => commandsMap.get(cmd.type)?.label || cmd.type);
const uniqueLabels = Array.from(new Set(selectedLabels));

Expand All @@ -111,15 +117,55 @@ export function CommandEditor() {
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Background Color Picker */}
{/* Color Pickers - Inline Side by Side */}
<div className="space-y-2">
<Label htmlFor="backgroundColor">{t('editor.backgroundColor')}</Label>
<ColorPicker
value={bgColorValue}
onChange={(value) => handlePropertyChange("backgroundColor", value)}
placeholder={isBgMixed ? '-' : undefined}
isMixed={isBgMixed}
/>
<div className="flex items-center gap-6">
{/* Background Color Picker */}
<div className="flex items-center gap-2">
<Label htmlFor="backgroundColor" className="min-w-[60px]">{t('editor.backgroundColor')}</Label>
<Popover>
<PopoverTrigger asChild>
<button
className="w-8 h-8 rounded border border-zinc-200 dark:border-zinc-800 cursor-pointer"
style={{ backgroundColor: bgColorValue }}
aria-label="Pick background color"
/>
</PopoverTrigger>
<PopoverContent className="w-auto p-3">
<HexColorPicker color={bgColorValue} onChange={(value) => handlePropertyChange("backgroundColor", value)} />
</PopoverContent>
</Popover>
<Input
value={isBgMixed ? '' : bgColorValue}
onChange={(e) => handlePropertyChange("backgroundColor", e.target.value)}
placeholder={isBgMixed ? '-' : undefined}
className="w-28"
/>
</div>

{/* Command Label Color Picker */}
<div className="flex items-center gap-2">
<Label htmlFor="commandLabelColor" className="min-w-[60px]">{t('editor.commandLabelColor')}</Label>
<Popover>
<PopoverTrigger asChild>
<button
className="w-8 h-8 rounded border border-zinc-200 dark:border-zinc-800 cursor-pointer"
style={{ backgroundColor: labelColorValue }}
aria-label="Pick text color"
/>
</PopoverTrigger>
<PopoverContent className="w-auto p-3">
<HexColorPicker color={labelColorValue} onChange={(value) => handlePropertyChange("commandLabelColor", value)} />
</PopoverContent>
</Popover>
<Input
value={isLabelColorMixed ? '' : labelColorValue}
onChange={(e) => handlePropertyChange("commandLabelColor", e.target.value)}
placeholder={isLabelColorMixed ? '-' : undefined}
className="w-28"
/>
</div>
</div>
</div>

{/* Command Properties */}
Expand Down
18 changes: 14 additions & 4 deletions frontend/src/components/skit/CommandList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -351,9 +351,10 @@ const CommandItem = memo(({
return lines;
}, [nestLevel, command.type]);

// Get command definition to check for defaultBackgroundColor
// Get command definition to check for defaultBackgroundColor and defaultCommandLabelColor
const commandDef = commandsMap?.get(command.type);
const defaultBgColor = commandDef?.defaultBackgroundColor || "#ffffff";
const defaultLabelColor = commandDef?.defaultCommandLabelColor || "";

const bgColor = command.backgroundColor || defaultBgColor;

Expand All @@ -362,7 +363,12 @@ const CommandItem = memo(({
...(isSelected ? {} : { backgroundColor: bgColor })
};

const getTextColor = (bgColor: string) => {
const getTextColorClass = (bgColor: string, labelColor?: string) => {
if (labelColor) {
// If label color is set, use it directly as inline style
return '';
}

if (!bgColor || bgColor === '') return '';

const hex = bgColor.replace('#', '');
Expand All @@ -375,7 +381,11 @@ const CommandItem = memo(({
return brightness < 128 ? 'text-white' : 'text-black';
};

const textColorClass = getTextColor(bgColor);
const labelColor = command.commandLabelColor || defaultLabelColor;
const textColorClass = getTextColorClass(bgColor, labelColor);

// Add inline style for custom label color
const textColorStyle = labelColor && !isSelected ? { color: labelColor } : {};

return (
<div
Expand All @@ -386,7 +396,7 @@ const CommandItem = memo(({
} ${isActive ? 'opacity-50' : ''}`}
onClick={(e) => handleCommandClick(command.id, e)}
data-testid={`command-item-${command.id}`}
style={backgroundColorStyle}
style={{...backgroundColorStyle, ...textColorStyle}}
>
{/* ネストレベルを示す垂直ライン */}
{nestLines}
Expand Down
26 changes: 25 additions & 1 deletion frontend/src/components/skit/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
Save,
ChevronDown,
ClipboardPaste,
Scissors
Scissors,
FolderOpen
} from 'lucide-react';
import { SidebarTrigger } from '../ui/sidebar';
import {
Expand All @@ -25,6 +26,7 @@ import { DraggableCommand } from '../dnd/DraggableCommand';
import { DropZone } from '../dnd/DropZone';
import { useTranslation } from 'react-i18next';
import { useCommandTranslation } from '../../hooks/useCommandTranslation';
import { openProjectDirectory } from '../../utils/fileSystem';

// Helper component to display translated command label
function CommandMenuLabel({ commandId, label }: { commandId: string; label?: string }) {
Expand All @@ -47,6 +49,7 @@ export function Toolbar() {
redo,
saveSkit,
commandDefinitions,
projectPath,
} = useSkitStore();

const handleAddCommand = (commandType: string) => {
Expand Down Expand Up @@ -83,6 +86,16 @@ export function Toolbar() {
}
};

const handleOpenProjectDirectory = async () => {
if (!projectPath) return;

try {
await openProjectDirectory(projectPath);
} catch (error) {
toast.error(t('editor.toolbar.openProjectFolderFailed', { error: error instanceof Error ? error.message : String(error) }));
}
};

const isDisabled = !currentSkitId;
const isCommandSelected = selectedCommandIds.length > 0;

Expand Down Expand Up @@ -189,10 +202,21 @@ export function Toolbar() {
size="sm"
disabled={isDisabled}
onClick={handleSave}
className="mr-2"
>
<Save className="h-4 w-4 mr-1" />
{t('editor.menu.file.save')}
</Button>

<Button
variant="outline"
size="sm"
disabled={!projectPath}
onClick={handleOpenProjectDirectory}
>
<FolderOpen className="h-4 w-4 mr-1" />
{t('editor.toolbar.openProjectFolder')}
</Button>
</div>
</div>
</div>
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/i18n/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const editorTranslations = {
projectLoadFailed: 'Failed to load project',
skitSaved: 'Skit saved',
saveFailed: 'Failed to save: {{error}}',
openProjectFolder: 'Open project folder',
openProjectFolder: 'Open folder',
folder: 'Folder',
currentDirectory: 'Current Directory',
noFolderOpen: 'No folder open',
Expand Down Expand Up @@ -66,6 +66,7 @@ const editorTranslations = {
noCommandSelected: 'No command selected',
commandDefinitionNotFound: 'Command definition not found',
backgroundColor: 'Background Color',
commandLabelColor: 'Text Color',
itemsSelected: '{{count}} items selected',
newSkit: 'New Skit',
createNewSkit: 'Create New Skit',
Expand Down Expand Up @@ -122,7 +123,7 @@ const editorTranslations = {
projectLoadFailed: 'プロジェクトの読み込みに失敗しました',
skitSaved: 'スキットを保存しました',
saveFailed: '保存に失敗しました: {{error}}',
openProjectFolder: 'プロジェクトフォルダを開く',
openProjectFolder: 'フォルダを開く',
folder: 'フォルダ',
currentDirectory: '現在のディレクトリ',
noFolderOpen: 'フォルダが開かれていません',
Expand Down Expand Up @@ -154,6 +155,7 @@ const editorTranslations = {
noCommandSelected: 'コマンドが選択されていません',
commandDefinitionNotFound: 'コマンド定義が見つかりません',
backgroundColor: '背景色',
commandLabelColor: '文字色',
itemsSelected: '{{count}}個選択中',
newSkit: '新規作成',
createNewSkit: '新規スキット作成',
Expand Down
16 changes: 15 additions & 1 deletion frontend/src/store/skitStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -758,7 +758,21 @@ export const useSkitStore = create<SkitState>()(
},

pasteCommandsFromClipboard: async () => {
const text = await navigator.clipboard.readText();
let text: string;
try {
// Try to use Tauri's clipboard API if available
if (typeof window !== 'undefined' && '__TAURI__' in window) {
const { invoke } = await import('@tauri-apps/api');
text = await invoke<string>('read_clipboard_text');
} else {
// Fallback to browser API for development
text = await navigator.clipboard.readText();
}
} catch (error) {
console.error('Failed to read clipboard:', error);
return;
}

let commands: SkitCommand[];
try {
commands = JSON.parse(text) as SkitCommand[];
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export interface CommandDefinition {
description: string;
commandListLabelFormat: string;
defaultBackgroundColor?: string;
defaultCommandLabelColor?: string;
properties: Record<string, PropertyDefinition>;
}

Expand All @@ -54,6 +55,7 @@ export interface SkitCommand {
id: number;
type: string;
backgroundColor?: string; // Background color for the command
commandLabelColor?: string; // Text color for the command
isCollapsed?: boolean; // For group_start commands
groupName?: string; // For group_start commands
// 必須プロパティの型も含めたインデックスシグネチャ
Expand Down
Loading
Loading