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
2 changes: 1 addition & 1 deletion .github/workflows/rust-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,4 @@ jobs:
- name: Run clippy
run: |
cd src-tauri
cargo clippy --all-targets --all-features -- -D warnings
cargo clippy --all-targets -- -D warnings
2 changes: 2 additions & 0 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ tauri = { version = "2.0", features = ["tray-icon", "devtools"] }
tauri-plugin-store = "2.0"
dirs = "5.0"

[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.6"
objc2-app-kit = { version = "0.3", features = ["NSApplication", "NSWindow", "NSRunningApplication"] }

[features]
# this feature is used for production builds or when `devPath` points to the filesystem
# DO NOT REMOVE!!
Expand Down
7 changes: 7 additions & 0 deletions src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@
"core:window:allow-set-resizable",
"core:window:allow-set-decorations",
"core:window:allow-set-always-on-top",
"core:window:allow-set-fullscreen",
"core:window:allow-set-max-size",
"core:window:allow-set-min-size",
"core:window:allow-set-focus",
"core:window:allow-set-position",
"core:window:allow-current-monitor",
"core:window:allow-outer-position",
"store:default"
]
}
2 changes: 1 addition & 1 deletion src-tauri/gen/schemas/capabilities.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"main-capability":{"identifier":"main-capability","description":"Main window capabilities","local":true,"windows":["main"],"permissions":["core:default","core:window:allow-set-always-on-top","core:window:allow-create","core:window:allow-close","core:window:allow-center","core:window:allow-set-size","core:window:allow-set-resizable","core:window:allow-set-decorations","core:window:allow-set-always-on-top","store:default"]},"timeup-capability":{"identifier":"timeup-capability","description":"Time Up window capabilities","local":true,"windows":["timeup"],"permissions":["core:default","core:window:allow-close"]}}
{"main-capability":{"identifier":"main-capability","description":"Main window capabilities","local":true,"windows":["main"],"permissions":["core:default","core:window:allow-set-always-on-top","core:window:allow-create","core:window:allow-close","core:window:allow-center","core:window:allow-set-size","core:window:allow-set-resizable","core:window:allow-set-decorations","core:window:allow-set-always-on-top","core:window:allow-set-fullscreen","core:window:allow-set-max-size","core:window:allow-set-min-size","core:window:allow-set-focus","core:window:allow-set-position","core:window:allow-current-monitor","core:window:allow-outer-position","store:default"]},"timeup-capability":{"identifier":"timeup-capability","description":"Time Up window capabilities","local":true,"windows":["timeup"],"permissions":["core:default","core:window:allow-close"]}}
43 changes: 38 additions & 5 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ use std::net::{SocketAddr, TcpListener};
use tauri::{AppHandle, Manager, WindowEvent};
use tauri_plugin_store::Builder as StoreBuilder;

#[cfg(target_os = "macos")]
use objc2::MainThreadMarker;
#[cfg(target_os = "macos")]
use objc2_app_kit::NSApplication;

#[derive(Serialize, Deserialize, Default, Debug)]
pub struct WindowState {
x: Option<i32>,
Expand Down Expand Up @@ -83,6 +88,37 @@ fn restore_window_state(window: &tauri::WebviewWindow) -> Result<(), Box<dyn std
Ok(())
}

/// macOSでネイティブAPIを使ってウィンドウとWKWebViewにフォーカスを強制的に戻すヘルパー関数
fn force_focus_window(window: &tauri::WebviewWindow) {
let _ = window.show();

#[cfg(target_os = "macos")]
{
let _ = window.with_webview(|webview| unsafe {
let mtm = MainThreadMarker::new_unchecked();
let ns_app = NSApplication::sharedApplication(mtm);
#[allow(deprecated)]
ns_app.activateIgnoringOtherApps(true);

let ns_window: &objc2_app_kit::NSWindow =
&*(webview.ns_window() as *const objc2_app_kit::NSWindow);
ns_window.makeKeyAndOrderFront(None);

// WKWebViewをFirst Responderに設定してキーボードイベントを受け取れるようにする
let wk_webview = webview.inner();
let responder = &*(wk_webview as *const objc2_app_kit::NSResponder);
ns_window.makeFirstResponder(Some(responder));
println!("DEBUG: WKWebView set as first responder");
});
println!("DEBUG: macOS native force focus applied");
}

#[cfg(not(target_os = "macos"))]
{
let _ = window.set_focus();
}
}

#[tauri::command]
async fn open_devtools(app: AppHandle) -> Result<(), String> {
if let Some(window) = app.get_webview_window("main") {
Expand Down Expand Up @@ -187,11 +223,8 @@ async fn set_window_resizable(app: AppHandle, resizable: bool) -> Result<(), Str
#[tauri::command]
async fn focus_window(app: AppHandle) -> Result<(), String> {
if let Some(window) = app.get_webview_window("main") {
if let Err(e) = window.set_focus() {
println!("DEBUG: Failed to focus window: {}", e);
return Err(format!("Failed to focus window: {}", e));
}
println!("DEBUG: Window focused");
force_focus_window(&window);
println!("DEBUG: Window force-focused");
}
Ok(())
}
Expand Down
6 changes: 2 additions & 4 deletions src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,8 @@
"title": "Lightning Timer",
"width": 800,
"height": 200,
"minWidth": 400,
"minHeight": 200,
"maxWidth": 800,
"maxHeight": 200,
"minWidth": 200,
"minHeight": 100,
"resizable": true,
"alwaysOnTop": false,
"decorations": false,
Expand Down
151 changes: 143 additions & 8 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState, useEffect, useCallback, useRef } from "react";
import { invoke, isTauri } from "@tauri-apps/api/core";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { getCurrentWindow, currentMonitor, LogicalSize, PhysicalPosition } from "@tauri-apps/api/window";
import { Store } from "@tauri-apps/plugin-store";
import TimerDisplay from "./components/TimerDisplay";
import TimerControls from "./components/TimerControls";
Expand Down Expand Up @@ -42,6 +42,12 @@ const App: React.FC = () => {
// TimeUP表示の状態管理
const [showTimeUp, setShowTimeUp] = useState(false);

// フルスクリーン状態管理
const [isFullscreen, setIsFullscreen] = useState(false);

// フルスクリーン前のウィンドウ位置を保存
const savedWindowGeometryRef = useRef<{ x: number; y: number } | null>(null);

// アラーム再生フラグ(重複再生を防ぐ)
const alarmPlayedRef = useRef(false);

Expand Down Expand Up @@ -509,15 +515,108 @@ const App: React.FC = () => {
height = 100;
}

const currentWindow = getCurrentWindow();

// フルスクリーン中はモード切り替え時に解除
if (isFullscreen) {
setIsFullscreen(false);
// 保存しておいた位置に戻す
if (savedWindowGeometryRef.current) {
const { x, y } = savedWindowGeometryRef.current;
await currentWindow.setPosition(new PhysicalPosition(x, y));
savedWindowGeometryRef.current = null;
}
}

// サイズ制約を一旦解除してからリサイズ
await currentWindow.setMinSize(null);
await currentWindow.setMaxSize(null);
await invoke("set_window_size", { width, height });

// ウィンドウのリサイズを無効化
await invoke("set_window_resizable", { resizable: false });
// min/maxサイズ制約でリサイズを防止
const size = new LogicalSize(width, height);
await currentWindow.setMinSize(size);
await currentWindow.setMaxSize(size);
} catch (error) {
console.error("Failed to set window size:", error);
}
}
}, [settings]);
}, [settings, isFullscreen]);

// フルスクリーン切り替え(手動フルスクリーン:ネイティブのsetFullscreenはmacOSデュアルディスプレイで問題があるため使わない)
const handleFullscreenToggle = useCallback(async () => {
if (!isTauri()) return;

try {
const currentWindow = getCurrentWindow();
const newFullscreen = !isFullscreen;

if (newFullscreen) {
// フルスクリーン前のウィンドウ位置を保存
const position = await currentWindow.outerPosition();
savedWindowGeometryRef.current = { x: position.x, y: position.y };

// 現在のモニター情報を取得
const monitor = await currentMonitor();
if (!monitor) {
console.error("Failed to get current monitor");
return;
}

// サイズ制約を解除してモニターサイズに拡大
await currentWindow.setMinSize(null);
await currentWindow.setMaxSize(null);

// モニターの論理サイズを計算
const logicalWidth = Math.round(monitor.size.width / monitor.scaleFactor);
const logicalHeight = Math.round(monitor.size.height / monitor.scaleFactor);

// モニターの左上に移動してから全画面サイズに変更
await currentWindow.setPosition(monitor.position);
await invoke("set_window_size", { width: logicalWidth, height: logicalHeight });
} else {
// 元のモードのウィンドウサイズに戻す
let width: number;
let height: number;
if (settings.displayMode === "compact") {
width = 400;
height = 200;
} else if (settings.displayMode === "minimal") {
width = 200;
height = 100;
} else {
width = 800;
height = 200;
}

// サイズ制約を一旦解除してからリサイズ
await currentWindow.setMinSize(null);
await currentWindow.setMaxSize(null);

// 保存しておいた位置に先に戻す(リサイズ前にモニター内に戻す)
if (savedWindowGeometryRef.current) {
const { x, y } = savedWindowGeometryRef.current;
await currentWindow.setPosition(new PhysicalPosition(x, y));
savedWindowGeometryRef.current = null;
}

await invoke("set_window_size", { width, height });

// min/maxサイズ制約でリサイズを防止
const size = new LogicalSize(width, height);
await currentWindow.setMinSize(size);
await currentWindow.setMaxSize(size);
}

setIsFullscreen(newFullscreen);

// WKWebViewのフォーカスを復帰
await new Promise(resolve => setTimeout(resolve, 50));
await invoke("focus_window");
} catch (error) {
console.error("Failed to toggle fullscreen:", error);
}
}, [isFullscreen, settings.displayMode]);

// キーボードイベントハンドラー
const handleKeyDown = useCallback(
Expand Down Expand Up @@ -562,6 +661,13 @@ const App: React.FC = () => {
}
}

// Cmd+Enter (Mac) / Ctrl+Enter (Windows/Linux) でフルスクリーン切り替え
else if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
handleFullscreenToggle();
return;
}

// S、Space、EnterキーでSTART/PAUSE操作
else if (
key === "s" ||
Expand Down Expand Up @@ -616,17 +722,35 @@ const App: React.FC = () => {
height = 100;
}

const currentWindow = getCurrentWindow();

// フルスクリーン中はモード切り替え時に解除
if (isFullscreen) {
setIsFullscreen(false);
// 保存しておいた位置に戻す
if (savedWindowGeometryRef.current) {
const { x, y } = savedWindowGeometryRef.current;
await currentWindow.setPosition(new PhysicalPosition(x, y));
savedWindowGeometryRef.current = null;
}
}

// サイズ制約を一旦解除してからリサイズ
await currentWindow.setMinSize(null);
await currentWindow.setMaxSize(null);
await invoke("set_window_size", { width, height });

// ウィンドウのリサイズを無効化
await invoke("set_window_resizable", { resizable: false });
// min/maxサイズ制約でリサイズを防止
const size = new LogicalSize(width, height);
await currentWindow.setMinSize(size);
await currentWindow.setMaxSize(size);
} catch (error) {
console.error("Failed to set window size:", error);
}
}
}
},
[timerState, startTimer, pauseTimer, resetTimer, updateTimerBoth, settings, stopAlarm]
[timerState, startTimer, pauseTimer, resetTimer, updateTimerBoth, settings, stopAlarm, handleFullscreenToggle, isFullscreen]
);

// キーボードイベントリスナーの設定
Expand Down Expand Up @@ -679,7 +803,7 @@ const App: React.FC = () => {

return (
<div
className={`app ${settings.darkMode ? "dark" : "light"} ${settings.displayMode === "compact" ? "compact" : ""} ${settings.displayMode === "minimal" ? "minimal" : ""}`}
className={`app ${settings.darkMode ? "dark" : "light"} ${settings.displayMode === "compact" ? "compact" : ""} ${settings.displayMode === "minimal" ? "minimal" : ""} ${isFullscreen ? "fullscreen" : ""}`}
onMouseDown={handleDragStart}
onMouseUp={handleMouseUp}
>
Expand Down Expand Up @@ -707,6 +831,17 @@ const App: React.FC = () => {
"⊞"}
</button>

{/* フルスクリーン切り替えボタン(右上) - 簡易モードのみ表示 */}
{settings.displayMode === "compact" && (
<button
className="fullscreen-toggle-button"
onClick={handleFullscreenToggle}
title={isFullscreen ? "ウィンドウモードに戻す" : "フルスクリーン"}
>
{isFullscreen ? "⤡" : "⤢"}
</button>
)}

{/* 情報アイコンボタン(右下) - 通常モードと簡易モードのみ表示 */}
{settings.displayMode !== "minimal" && (
<button
Expand Down
1 change: 1 addition & 0 deletions src/components/Help.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const Help: React.FC<HelpProps> = ({ onClose }) => {
{ key: "Enter", description: "Start/Pause timer" },
{ key: "R", description: "Reset timer" },
{ key: "V", description: "Toggle compact mode" },
{ key: "Cmd/Ctrl+Enter", description: "Toggle fullscreen" },
];

return (
Expand Down
Loading