fix header name, modulize windowmanager, fix ui size bug
This commit is contained in:
parent
a18e93583f
commit
12a07b8607
@ -70,6 +70,7 @@
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-darwin-x64": "^0.34.2",
|
||||
"@img/sharp-libvips-darwin-x64": "^1.1.0"
|
||||
"@img/sharp-libvips-darwin-x64": "^1.1.0",
|
||||
"electron-liquid-glass": "^1.0.1"
|
||||
}
|
||||
}
|
||||
|
@ -248,6 +248,30 @@ export class ApiKeyHeader extends LitElement {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
:host-context(body.has-glass) .container,
|
||||
:host-context(body.has-glass) .api-input,
|
||||
:host-context(body.has-glass) .provider-select,
|
||||
:host-context(body.has-glass) .action-button,
|
||||
:host-context(body.has-glass) .close-button {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
filter: none !important;
|
||||
backdrop-filter: none !important;
|
||||
}
|
||||
|
||||
/* 가상 레이어·그라데이션 테두리 제거 */
|
||||
:host-context(body.has-glass) .container::after,
|
||||
:host-context(body.has-glass) .action-button::after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* hover/active 때 다시 생기는 배경도 차단 */
|
||||
:host-context(body.has-glass) .action-button:hover,
|
||||
:host-context(body.has-glass) .provider-select:hover,
|
||||
:host-context(body.has-glass) .close-button:hover {
|
||||
background: transparent !important;
|
||||
}
|
||||
`;
|
||||
|
||||
constructor() {
|
||||
|
@ -1,18 +1,18 @@
|
||||
import './AppHeader.js';
|
||||
import './MainHeader.js';
|
||||
import './ApiKeyHeader.js';
|
||||
import './PermissionSetup.js';
|
||||
import './PermissionHeader.js';
|
||||
|
||||
class HeaderTransitionManager {
|
||||
constructor() {
|
||||
this.headerContainer = document.getElementById('header-container');
|
||||
this.currentHeaderType = null; // 'apikey' | 'app' | 'permission'
|
||||
this.currentHeaderType = null; // 'apikey' | 'main' | 'permission'
|
||||
this.apiKeyHeader = null;
|
||||
this.appHeader = null;
|
||||
this.permissionSetup = null;
|
||||
this.mainHeader = null;
|
||||
this.permissionHeader = null;
|
||||
|
||||
/**
|
||||
* only one header window is allowed
|
||||
* @param {'apikey'|'app'|'permission'} type
|
||||
* @param {'apikey'|'main'|'permission'} type
|
||||
*/
|
||||
this.ensureHeader = (type) => {
|
||||
if (this.currentHeaderType === type) return;
|
||||
@ -20,21 +20,21 @@ class HeaderTransitionManager {
|
||||
this.headerContainer.innerHTML = '';
|
||||
|
||||
this.apiKeyHeader = null;
|
||||
this.appHeader = null;
|
||||
this.permissionSetup = null;
|
||||
this.mainHeader = null;
|
||||
this.permissionHeader = null;
|
||||
|
||||
// Create new header element
|
||||
if (type === 'apikey') {
|
||||
this.apiKeyHeader = document.createElement('apikey-header');
|
||||
this.headerContainer.appendChild(this.apiKeyHeader);
|
||||
} else if (type === 'permission') {
|
||||
this.permissionSetup = document.createElement('permission-setup');
|
||||
this.permissionSetup.continueCallback = () => this.transitionToAppHeader();
|
||||
this.headerContainer.appendChild(this.permissionSetup);
|
||||
this.permissionHeader = document.createElement('permission-setup');
|
||||
this.permissionHeader.continueCallback = () => this.transitionToMainHeader();
|
||||
this.headerContainer.appendChild(this.permissionHeader);
|
||||
} else {
|
||||
this.appHeader = document.createElement('app-header');
|
||||
this.headerContainer.appendChild(this.appHeader);
|
||||
this.appHeader.startSlideInAnimation?.();
|
||||
this.mainHeader = document.createElement('main-header');
|
||||
this.headerContainer.appendChild(this.mainHeader);
|
||||
this.mainHeader.startSlideInAnimation?.();
|
||||
}
|
||||
|
||||
this.currentHeaderType = type;
|
||||
@ -87,16 +87,16 @@ class HeaderTransitionManager {
|
||||
const { isLoggedIn, hasApiKey } = userState;
|
||||
|
||||
if (isLoggedIn) {
|
||||
// Firebase user: Check permissions, then show App or Permission Setup
|
||||
// Firebase user: Check permissions, then show Main or Permission header
|
||||
const permissionResult = await this.checkPermissions();
|
||||
if (permissionResult.success) {
|
||||
this.transitionToAppHeader();
|
||||
this.transitionToMainHeader();
|
||||
} else {
|
||||
this.transitionToPermissionSetup();
|
||||
this.transitionToPermissionHeader();
|
||||
}
|
||||
} else if (hasApiKey) {
|
||||
// API Key only user: Skip permission check, go directly to App
|
||||
this.transitionToAppHeader();
|
||||
// API Key only user: Skip permission check, go directly to Main
|
||||
this.transitionToMainHeader();
|
||||
} else {
|
||||
// No auth at all
|
||||
await this._resizeForApiKey();
|
||||
@ -104,7 +104,7 @@ class HeaderTransitionManager {
|
||||
}
|
||||
}
|
||||
|
||||
async transitionToPermissionSetup() {
|
||||
async transitionToPermissionHeader() {
|
||||
// Prevent duplicate transitions
|
||||
if (this.currentHeaderType === 'permission') {
|
||||
console.log('[HeaderController] Already showing permission setup, skipping transition');
|
||||
@ -123,7 +123,7 @@ class HeaderTransitionManager {
|
||||
const permissionResult = await this.checkPermissions();
|
||||
if (permissionResult.success) {
|
||||
// Skip permission setup if already granted
|
||||
this.transitionToAppHeader();
|
||||
this.transitionToMainHeader();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -134,24 +134,24 @@ class HeaderTransitionManager {
|
||||
}
|
||||
}
|
||||
|
||||
await this._resizeForPermissionSetup();
|
||||
await this._resizeForPermissionHeader();
|
||||
this.ensureHeader('permission');
|
||||
}
|
||||
|
||||
async transitionToAppHeader(animate = true) {
|
||||
if (this.currentHeaderType === 'app') {
|
||||
return this._resizeForApp();
|
||||
async transitionToMainHeader(animate = true) {
|
||||
if (this.currentHeaderType === 'main') {
|
||||
return this._resizeForMain();
|
||||
}
|
||||
|
||||
await this._resizeForApp();
|
||||
this.ensureHeader('app');
|
||||
await this._resizeForMain();
|
||||
this.ensureHeader('main');
|
||||
}
|
||||
|
||||
_resizeForApp() {
|
||||
_resizeForMain() {
|
||||
if (!window.require) return;
|
||||
return window
|
||||
.require('electron')
|
||||
.ipcRenderer.invoke('resize-header-window', { width: 353, height: 60 })
|
||||
.ipcRenderer.invoke('resize-header-window', { width: 353, height: 47 })
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
@ -163,7 +163,7 @@ class HeaderTransitionManager {
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
async _resizeForPermissionSetup() {
|
||||
async _resizeForPermissionHeader() {
|
||||
if (!window.require) return;
|
||||
return window
|
||||
.require('electron')
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { html, css, LitElement } from '../assets/lit-core-2.7.4.min.js';
|
||||
|
||||
export class AppHeader extends LitElement {
|
||||
export class MainHeader extends LitElement {
|
||||
static properties = {
|
||||
isSessionActive: { type: Boolean, state: true },
|
||||
};
|
||||
@ -292,6 +292,58 @@ export class AppHeader extends LitElement {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
:host-context(body.has-glass) .header,
|
||||
:host-context(body.has-glass) .listen-button,
|
||||
:host-context(body.has-glass) .header-actions,
|
||||
:host-context(body.has-glass) .settings-button {
|
||||
/* 배경·블러·그림자 전부 제거 */
|
||||
background: transparent !important;
|
||||
filter: none !important;
|
||||
box-shadow: none !important;
|
||||
backdrop-filter: none !important;
|
||||
}
|
||||
:host-context(body.has-glass) .icon-box {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
/* 장식용 before/after 레이어와 버튼 오버레이 비활성화 */
|
||||
:host-context(body.has-glass) .header::before,
|
||||
:host-context(body.has-glass) .header::after,
|
||||
:host-context(body.has-glass) .listen-button::before,
|
||||
:host-context(body.has-glass) .listen-button::after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* hover 때 의도치 않게 생기는 배경도 차단 */
|
||||
:host-context(body.has-glass) .header-actions:hover,
|
||||
:host-context(body.has-glass) .settings-button:hover,
|
||||
:host-context(body.has-glass) .listen-button:hover::before {
|
||||
background: transparent !important;
|
||||
}
|
||||
:host-context(body.has-glass) * {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
transform: none !important;
|
||||
filter: none !important;
|
||||
backdrop-filter: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* 2) pill 형태·아이콘 박스 둥근 모서리 평면화 (선택) */
|
||||
:host-context(body.has-glass) .header,
|
||||
:host-context(body.has-glass) .listen-button,
|
||||
:host-context(body.has-glass) .header-actions,
|
||||
:host-context(body.has-glass) .settings-button,
|
||||
:host-context(body.has-glass) .icon-box {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
:host-context(body.has-glass) {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
transform: none !important;
|
||||
will-change: auto !important;
|
||||
}
|
||||
`;
|
||||
|
||||
constructor() {
|
||||
@ -362,7 +414,7 @@ export class AppHeader extends LitElement {
|
||||
|
||||
toggleVisibility() {
|
||||
if (this.isAnimating) {
|
||||
console.log('[AppHeader] Animation already in progress, ignoring toggle');
|
||||
console.log('[MainHeader] Animation already in progress, ignoring toggle');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -432,7 +484,7 @@ export class AppHeader extends LitElement {
|
||||
} else if (this.classList.contains('sliding-in')) {
|
||||
this.classList.remove('sliding-in');
|
||||
this.hasSlidIn = true;
|
||||
console.log('[AppHeader] Slide-in animation completed');
|
||||
console.log('[MainHeader] Slide-in animation completed');
|
||||
}
|
||||
}
|
||||
|
||||
@ -484,7 +536,7 @@ export class AppHeader extends LitElement {
|
||||
if (this.wasJustDragged) return;
|
||||
if (window.require) {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
console.log(`[AppHeader] showWindow('${name}') called at ${Date.now()}`);
|
||||
console.log(`[MainHeader] showWindow('${name}') called at ${Date.now()}`);
|
||||
|
||||
ipcRenderer.send('cancel-hide-window', name);
|
||||
|
||||
@ -508,7 +560,7 @@ export class AppHeader extends LitElement {
|
||||
hideWindow(name) {
|
||||
if (this.wasJustDragged) return;
|
||||
if (window.require) {
|
||||
console.log(`[AppHeader] hideWindow('${name}') called at ${Date.now()}`);
|
||||
console.log(`[MainHeader] hideWindow('${name}') called at ${Date.now()}`);
|
||||
window.require('electron').ipcRenderer.send('hide-window', name);
|
||||
}
|
||||
}
|
||||
@ -590,4 +642,4 @@ export class AppHeader extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('app-header', AppHeader);
|
||||
customElements.define('main-header', MainHeader);
|
@ -1,6 +1,6 @@
|
||||
import { LitElement, html, css } from '../assets/lit-core-2.7.4.min.js';
|
||||
|
||||
export class PermissionSetup extends LitElement {
|
||||
export class PermissionHeader extends LitElement {
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
@ -237,6 +237,30 @@ export class PermissionSetup extends LitElement {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
:host-context(body.has-glass) .container,
|
||||
:host-context(body.has-glass) .action-button,
|
||||
:host-context(body.has-glass) .continue-button,
|
||||
:host-context(body.has-glass) .close-button {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
filter: none !important;
|
||||
backdrop-filter: none !important;
|
||||
}
|
||||
|
||||
/* Remove gradient borders / pseudo layers */
|
||||
:host-context(body.has-glass) .container::after,
|
||||
:host-context(body.has-glass) .action-button::after,
|
||||
:host-context(body.has-glass) .continue-button::after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Prevent background reappearing on hover/active */
|
||||
:host-context(body.has-glass) .action-button:hover,
|
||||
:host-context(body.has-glass) .continue-button:hover,
|
||||
:host-context(body.has-glass) .close-button:hover {
|
||||
background: transparent !important;
|
||||
}
|
||||
`;
|
||||
|
||||
static properties = {
|
||||
@ -337,7 +361,7 @@ export class PermissionSetup extends LitElement {
|
||||
|
||||
try {
|
||||
const permissions = await ipcRenderer.invoke('check-system-permissions');
|
||||
console.log('[PermissionSetup] Permission check result:', permissions);
|
||||
console.log('[PermissionHeader] Permission check result:', permissions);
|
||||
|
||||
const prevMic = this.microphoneGranted;
|
||||
const prevScreen = this.screenGranted;
|
||||
@ -347,7 +371,7 @@ export class PermissionSetup extends LitElement {
|
||||
|
||||
// if permissions changed == UI update
|
||||
if (prevMic !== this.microphoneGranted || prevScreen !== this.screenGranted) {
|
||||
console.log('[PermissionSetup] Permission status changed, updating UI');
|
||||
console.log('[PermissionHeader] Permission status changed, updating UI');
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
@ -355,11 +379,11 @@ export class PermissionSetup extends LitElement {
|
||||
if (this.microphoneGranted === 'granted' &&
|
||||
this.screenGranted === 'granted' &&
|
||||
this.continueCallback) {
|
||||
console.log('[PermissionSetup] All permissions granted, proceeding automatically');
|
||||
console.log('[PermissionHeader] All permissions granted, proceeding automatically');
|
||||
setTimeout(() => this.handleContinue(), 500);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[PermissionSetup] Error checking permissions:', error);
|
||||
console.error('[PermissionHeader] Error checking permissions:', error);
|
||||
} finally {
|
||||
this.isChecking = false;
|
||||
}
|
||||
@ -368,12 +392,12 @@ export class PermissionSetup extends LitElement {
|
||||
async handleMicrophoneClick() {
|
||||
if (!window.require || this.microphoneGranted === 'granted' || this.wasJustDragged) return;
|
||||
|
||||
console.log('[PermissionSetup] Requesting microphone permission...');
|
||||
console.log('[PermissionHeader] Requesting microphone permission...');
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
|
||||
try {
|
||||
const result = await ipcRenderer.invoke('check-system-permissions');
|
||||
console.log('[PermissionSetup] Microphone permission result:', result);
|
||||
console.log('[PermissionHeader] Microphone permission result:', result);
|
||||
|
||||
if (result.microphone === 'granted') {
|
||||
this.microphoneGranted = 'granted';
|
||||
@ -394,19 +418,19 @@ export class PermissionSetup extends LitElement {
|
||||
// Check permissions again after a delay
|
||||
// setTimeout(() => this.checkPermissions(), 1000);
|
||||
} catch (error) {
|
||||
console.error('[PermissionSetup] Error requesting microphone permission:', error);
|
||||
console.error('[PermissionHeader] Error requesting microphone permission:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async handleScreenClick() {
|
||||
if (!window.require || this.screenGranted === 'granted' || this.wasJustDragged) return;
|
||||
|
||||
console.log('[PermissionSetup] Checking screen recording permission...');
|
||||
console.log('[PermissionHeader] Checking screen recording permission...');
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
|
||||
try {
|
||||
const permissions = await ipcRenderer.invoke('check-system-permissions');
|
||||
console.log('[PermissionSetup] Screen permission check result:', permissions);
|
||||
console.log('[PermissionHeader] Screen permission check result:', permissions);
|
||||
|
||||
if (permissions.screen === 'granted') {
|
||||
this.screenGranted = 'granted';
|
||||
@ -414,7 +438,7 @@ export class PermissionSetup extends LitElement {
|
||||
return;
|
||||
}
|
||||
if (permissions.screen === 'not-determined' || permissions.screen === 'denied' || permissions.screen === 'unknown' || permissions.screen === 'restricted') {
|
||||
console.log('[PermissionSetup] Opening screen recording preferences...');
|
||||
console.log('[PermissionHeader] Opening screen recording preferences...');
|
||||
await ipcRenderer.invoke('open-system-preferences', 'screen-recording');
|
||||
}
|
||||
|
||||
@ -422,7 +446,7 @@ export class PermissionSetup extends LitElement {
|
||||
// (This may not execute if app restarts after permission grant)
|
||||
// setTimeout(() => this.checkPermissions(), 2000);
|
||||
} catch (error) {
|
||||
console.error('[PermissionSetup] Error opening screen recording preferences:', error);
|
||||
console.error('[PermissionHeader] Error opening screen recording preferences:', error);
|
||||
}
|
||||
}
|
||||
|
||||
@ -436,9 +460,9 @@ export class PermissionSetup extends LitElement {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
try {
|
||||
await ipcRenderer.invoke('mark-permissions-completed');
|
||||
console.log('[PermissionSetup] Marked permissions as completed');
|
||||
console.log('[PermissionHeader] Marked permissions as completed');
|
||||
} catch (error) {
|
||||
console.error('[PermissionSetup] Error marking permissions as completed:', error);
|
||||
console.error('[PermissionHeader] Error marking permissions as completed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
@ -530,4 +554,4 @@ export class PermissionSetup extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('permission-setup', PermissionSetup);
|
||||
customElements.define('permission-setup', PermissionHeader);
|
@ -1,7 +1,6 @@
|
||||
import { html, css, LitElement } from '../assets/lit-core-2.7.4.min.js';
|
||||
import { CustomizeView } from '../features/customize/CustomizeView.js';
|
||||
import { AssistantView } from '../features/listen/AssistantView.js';
|
||||
import { OnboardingView } from '../features/onboarding/OnboardingView.js';
|
||||
import { AskView } from '../features/ask/AskView.js';
|
||||
|
||||
import '../features/listen/renderer.js';
|
||||
@ -11,6 +10,7 @@ export class PickleGlassApp extends LitElement {
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--text-color);
|
||||
background: transparent;
|
||||
border-radius: 7px;
|
||||
@ -19,11 +19,13 @@ export class PickleGlassApp extends LitElement {
|
||||
assistant-view {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
ask-view, customize-view, history-view, help-view, onboarding-view, setup-view {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
`;
|
||||
|
@ -301,5 +301,11 @@
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get('glass') === 'true') {
|
||||
document.body.classList.add('has-glass');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -15,10 +15,14 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="header-container" tabindex="0" style="outline: none;">
|
||||
<!-- <apikey-header id="apikey-header" style="display: none;"></apikey-header>
|
||||
<app-header id="app-header" style="display: none;"></app-header> -->
|
||||
</div>
|
||||
|
||||
<script type="module" src="../../public/build/header.js"></script>
|
||||
<script>
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get('glass') === 'true') {
|
||||
document.body.classList.add('has-glass');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
312
src/electron/smoothMovementManager.js
Normal file
312
src/electron/smoothMovementManager.js
Normal file
@ -0,0 +1,312 @@
|
||||
const { screen } = require('electron');
|
||||
|
||||
class SmoothMovementManager {
|
||||
constructor(windowPool, getDisplayById, getCurrentDisplay, updateLayout) {
|
||||
this.windowPool = windowPool;
|
||||
this.getDisplayById = getDisplayById;
|
||||
this.getCurrentDisplay = getCurrentDisplay;
|
||||
this.updateLayout = updateLayout;
|
||||
this.stepSize = 80;
|
||||
this.animationDuration = 300;
|
||||
this.headerPosition = { x: 0, y: 0 };
|
||||
this.isAnimating = false;
|
||||
this.hiddenPosition = null;
|
||||
this.lastVisiblePosition = null;
|
||||
this.currentDisplayId = null;
|
||||
this.animationFrameId = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {BrowserWindow} win
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_isWindowValid(win) {
|
||||
if (!win || win.isDestroyed()) {
|
||||
if (this.isAnimating) {
|
||||
console.warn('[MovementManager] Window destroyed mid-animation. Halting.');
|
||||
this.isAnimating = false;
|
||||
if (this.animationFrameId) {
|
||||
clearTimeout(this.animationFrameId);
|
||||
this.animationFrameId = null;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
moveToDisplay(displayId) {
|
||||
const header = this.windowPool.get('header');
|
||||
if (!this._isWindowValid(header) || !header.isVisible() || this.isAnimating) return;
|
||||
|
||||
const targetDisplay = this.getDisplayById(displayId);
|
||||
if (!targetDisplay) return;
|
||||
|
||||
const currentBounds = header.getBounds();
|
||||
const currentDisplay = this.getCurrentDisplay(header);
|
||||
|
||||
if (currentDisplay.id === targetDisplay.id) return;
|
||||
|
||||
const relativeX = (currentBounds.x - currentDisplay.workArea.x) / currentDisplay.workAreaSize.width;
|
||||
const relativeY = (currentBounds.y - currentDisplay.workArea.y) / currentDisplay.workAreaSize.height;
|
||||
const targetX = targetDisplay.workArea.x + targetDisplay.workAreaSize.width * relativeX;
|
||||
const targetY = targetDisplay.workArea.y + targetDisplay.workAreaSize.height * relativeY;
|
||||
|
||||
const finalX = Math.max(targetDisplay.workArea.x, Math.min(targetDisplay.workArea.x + targetDisplay.workAreaSize.width - currentBounds.width, targetX));
|
||||
const finalY = Math.max(targetDisplay.workArea.y, Math.min(targetDisplay.workArea.y + targetDisplay.workAreaSize.height - currentBounds.height, targetY));
|
||||
|
||||
this.headerPosition = { x: currentBounds.x, y: currentBounds.y };
|
||||
this.animateToPosition(header, finalX, finalY);
|
||||
this.currentDisplayId = targetDisplay.id;
|
||||
}
|
||||
|
||||
hideToEdge(edge, callback) {
|
||||
const header = this.windowPool.get('header');
|
||||
if (!this._isWindowValid(header) || !header.isVisible() || this.isAnimating) return;
|
||||
|
||||
const currentBounds = header.getBounds();
|
||||
const display = this.getCurrentDisplay(header);
|
||||
|
||||
if (
|
||||
!currentBounds || typeof currentBounds.x !== 'number' || typeof currentBounds.y !== 'number' ||
|
||||
!display || !display.workArea || !display.workAreaSize ||
|
||||
typeof display.workArea.x !== 'number' || typeof display.workArea.y !== 'number' ||
|
||||
typeof display.workAreaSize.width !== 'number' || typeof display.workAreaSize.height !== 'number'
|
||||
) {
|
||||
console.error('[MovementManager] Invalid bounds or display info for hideToEdge. Aborting.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastVisiblePosition = { x: currentBounds.x, y: currentBounds.y };
|
||||
this.headerPosition = { x: currentBounds.x, y: currentBounds.y };
|
||||
|
||||
const { width: screenWidth, height: screenHeight } = display.workAreaSize;
|
||||
const { x: workAreaX, y: workAreaY } = display.workArea;
|
||||
|
||||
let targetX = this.headerPosition.x;
|
||||
let targetY = this.headerPosition.y;
|
||||
|
||||
switch (edge) {
|
||||
case 'top': targetY = workAreaY - currentBounds.height - 20; break;
|
||||
case 'bottom': targetY = workAreaY + screenHeight + 20; break;
|
||||
case 'left': targetX = workAreaX - currentBounds.width - 20; break;
|
||||
case 'right': targetX = workAreaX + screenWidth + 20; break;
|
||||
}
|
||||
|
||||
this.hiddenPosition = { x: targetX, y: targetY, edge };
|
||||
this.isAnimating = true;
|
||||
const startX = this.headerPosition.x;
|
||||
const startY = this.headerPosition.y;
|
||||
const duration = 400;
|
||||
const startTime = Date.now();
|
||||
|
||||
const animate = () => {
|
||||
if (!this._isWindowValid(header)) return;
|
||||
|
||||
const elapsed = Date.now() - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
const eased = progress * progress * progress;
|
||||
const currentX = startX + (targetX - startX) * eased;
|
||||
const currentY = startY + (targetY - startY) * eased;
|
||||
|
||||
if (!Number.isFinite(currentX) || !Number.isFinite(currentY)) {
|
||||
this.isAnimating = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._isWindowValid(header)) return;
|
||||
header.setPosition(Math.round(currentX), Math.round(currentY));
|
||||
|
||||
if (progress < 1) {
|
||||
this.animationFrameId = setTimeout(animate, 8);
|
||||
} else {
|
||||
this.animationFrameId = null;
|
||||
this.headerPosition = { x: targetX, y: targetY };
|
||||
if (Number.isFinite(targetX) && Number.isFinite(targetY)) {
|
||||
if (!this._isWindowValid(header)) return;
|
||||
header.setPosition(Math.round(targetX), Math.round(targetY));
|
||||
}
|
||||
this.isAnimating = false;
|
||||
if (typeof callback === 'function') callback();
|
||||
}
|
||||
};
|
||||
animate();
|
||||
}
|
||||
|
||||
showFromEdge(callback) {
|
||||
const header = this.windowPool.get('header');
|
||||
if (
|
||||
!this._isWindowValid(header) || this.isAnimating ||
|
||||
!this.hiddenPosition || !this.lastVisiblePosition ||
|
||||
typeof this.hiddenPosition.x !== 'number' || typeof this.hiddenPosition.y !== 'number' ||
|
||||
typeof this.lastVisiblePosition.x !== 'number' || typeof this.lastVisiblePosition.y !== 'number'
|
||||
) {
|
||||
console.error('[MovementManager] Invalid state for showFromEdge. Aborting.');
|
||||
this.isAnimating = false;
|
||||
this.hiddenPosition = null;
|
||||
this.lastVisiblePosition = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._isWindowValid(header)) return;
|
||||
header.setPosition(this.hiddenPosition.x, this.hiddenPosition.y);
|
||||
|
||||
this.headerPosition = { x: this.hiddenPosition.x, y: this.hiddenPosition.y };
|
||||
const targetX = this.lastVisiblePosition.x;
|
||||
const targetY = this.lastVisiblePosition.y;
|
||||
this.isAnimating = true;
|
||||
const startX = this.headerPosition.x;
|
||||
const startY = this.headerPosition.y;
|
||||
const duration = 500;
|
||||
const startTime = Date.now();
|
||||
|
||||
const animate = () => {
|
||||
if (!this._isWindowValid(header)) return;
|
||||
|
||||
const elapsed = Date.now() - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
const c1 = 1.70158;
|
||||
const c3 = c1 + 1;
|
||||
const eased = 1 + c3 * Math.pow(progress - 1, 3) + c1 * Math.pow(progress - 1, 2);
|
||||
const currentX = startX + (targetX - startX) * eased;
|
||||
const currentY = startY + (targetY - startY) * eased;
|
||||
if (!Number.isFinite(currentX) || !Number.isFinite(currentY)) {
|
||||
this.isAnimating = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._isWindowValid(header)) return;
|
||||
header.setPosition(Math.round(currentX), Math.round(currentY));
|
||||
|
||||
if (progress < 1) {
|
||||
this.animationFrameId = setTimeout(animate, 8);
|
||||
} else {
|
||||
this.animationFrameId = null;
|
||||
this.headerPosition = { x: targetX, y: targetY };
|
||||
if (Number.isFinite(targetX) && Number.isFinite(targetY)) {
|
||||
if (!this._isWindowValid(header)) return;
|
||||
header.setPosition(Math.round(targetX), Math.round(targetY));
|
||||
}
|
||||
this.isAnimating = false;
|
||||
this.hiddenPosition = null;
|
||||
this.lastVisiblePosition = null;
|
||||
if (callback) callback();
|
||||
}
|
||||
};
|
||||
animate();
|
||||
}
|
||||
|
||||
moveStep(direction) {
|
||||
const header = this.windowPool.get('header');
|
||||
if (!this._isWindowValid(header) || !header.isVisible() || this.isAnimating) return;
|
||||
|
||||
const currentBounds = header.getBounds();
|
||||
this.headerPosition = { x: currentBounds.x, y: currentBounds.y };
|
||||
let targetX = this.headerPosition.x;
|
||||
let targetY = this.headerPosition.y;
|
||||
|
||||
switch (direction) {
|
||||
case 'left': targetX -= this.stepSize; break;
|
||||
case 'right': targetX += this.stepSize; break;
|
||||
case 'up': targetY -= this.stepSize; break;
|
||||
case 'down': targetY += this.stepSize; break;
|
||||
default: return;
|
||||
}
|
||||
|
||||
const displays = screen.getAllDisplays();
|
||||
let validPosition = displays.some(d => (
|
||||
targetX >= d.workArea.x && targetX + currentBounds.width <= d.workArea.x + d.workArea.width &&
|
||||
targetY >= d.workArea.y && targetY + currentBounds.height <= d.workArea.y + d.workArea.height
|
||||
));
|
||||
|
||||
if (!validPosition) {
|
||||
const nearestDisplay = screen.getDisplayNearestPoint({ x: targetX, y: targetY });
|
||||
const { x, y, width, height } = nearestDisplay.workArea;
|
||||
targetX = Math.max(x, Math.min(x + width - currentBounds.width, targetX));
|
||||
targetY = Math.max(y, Math.min(y + height - currentBounds.height, targetY));
|
||||
}
|
||||
|
||||
if (targetX === this.headerPosition.x && targetY === this.headerPosition.y) return;
|
||||
this.animateToPosition(header, targetX, targetY);
|
||||
}
|
||||
|
||||
animateToPosition(header, targetX, targetY) {
|
||||
if (!this._isWindowValid(header)) return;
|
||||
|
||||
this.isAnimating = true;
|
||||
const startX = this.headerPosition.x;
|
||||
const startY = this.headerPosition.y;
|
||||
const startTime = Date.now();
|
||||
|
||||
if (!Number.isFinite(targetX) || !Number.isFinite(targetY) || !Number.isFinite(startX) || !Number.isFinite(startY)) {
|
||||
this.isAnimating = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const animate = () => {
|
||||
if (!this._isWindowValid(header)) return;
|
||||
|
||||
const elapsed = Date.now() - startTime;
|
||||
const progress = Math.min(elapsed / this.animationDuration, 1);
|
||||
const eased = 1 - Math.pow(1 - progress, 3);
|
||||
const currentX = startX + (targetX - startX) * eased;
|
||||
const currentY = startY + (targetY - startY) * eased;
|
||||
|
||||
if (!Number.isFinite(currentX) || !Number.isFinite(currentY)) {
|
||||
this.isAnimating = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._isWindowValid(header)) return;
|
||||
header.setPosition(Math.round(currentX), Math.round(currentY));
|
||||
|
||||
if (progress < 1) {
|
||||
this.animationFrameId = setTimeout(animate, 8);
|
||||
} else {
|
||||
this.animationFrameId = null;
|
||||
this.headerPosition = { x: targetX, y: targetY };
|
||||
if (Number.isFinite(targetX) && Number.isFinite(targetY)) {
|
||||
if (!this._isWindowValid(header)) return;
|
||||
header.setPosition(Math.round(targetX), Math.round(targetY));
|
||||
}
|
||||
this.isAnimating = false;
|
||||
this.updateLayout();
|
||||
}
|
||||
};
|
||||
animate();
|
||||
}
|
||||
|
||||
moveToEdge(direction) {
|
||||
const header = this.windowPool.get('header');
|
||||
if (!this._isWindowValid(header) || !header.isVisible() || this.isAnimating) return;
|
||||
|
||||
const display = this.getCurrentDisplay(header);
|
||||
const { width, height } = display.workAreaSize;
|
||||
const { x: workAreaX, y: workAreaY } = display.workArea;
|
||||
const headerBounds = header.getBounds();
|
||||
const currentBounds = header.getBounds();
|
||||
let targetX = currentBounds.x;
|
||||
let targetY = currentBounds.y;
|
||||
|
||||
switch (direction) {
|
||||
case 'left': targetX = workAreaX; break;
|
||||
case 'right': targetX = workAreaX + width - headerBounds.width; break;
|
||||
case 'up': targetY = workAreaY; break;
|
||||
case 'down': targetY = workAreaY + height - headerBounds.height; break;
|
||||
}
|
||||
|
||||
this.headerPosition = { x: currentBounds.x, y: currentBounds.y };
|
||||
this.animateToPosition(header, targetX, targetY);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.animationFrameId) {
|
||||
clearTimeout(this.animationFrameId);
|
||||
this.animationFrameId = null;
|
||||
}
|
||||
this.isAnimating = false;
|
||||
console.log('[Movement] Manager destroyed');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SmoothMovementManager;
|
217
src/electron/windowLayoutManager.js
Normal file
217
src/electron/windowLayoutManager.js
Normal file
@ -0,0 +1,217 @@
|
||||
const { screen } = require('electron');
|
||||
|
||||
/**
|
||||
* 주어진 창이 현재 어느 디스플레이에 속해 있는지 반환합니다.
|
||||
* @param {BrowserWindow} window - 확인할 창 객체
|
||||
* @returns {Display} Electron의 Display 객체
|
||||
*/
|
||||
function getCurrentDisplay(window) {
|
||||
if (!window || window.isDestroyed()) return screen.getPrimaryDisplay();
|
||||
|
||||
const windowBounds = window.getBounds();
|
||||
const windowCenter = {
|
||||
x: windowBounds.x + windowBounds.width / 2,
|
||||
y: windowBounds.y + windowBounds.height / 2,
|
||||
};
|
||||
|
||||
return screen.getDisplayNearestPoint(windowCenter);
|
||||
}
|
||||
|
||||
class WindowLayoutManager {
|
||||
/**
|
||||
* @param {Map<string, BrowserWindow>} windowPool - 관리할 창들의 맵
|
||||
*/
|
||||
constructor(windowPool) {
|
||||
this.windowPool = windowPool;
|
||||
this.isUpdating = false;
|
||||
this.PADDING = 80;
|
||||
}
|
||||
|
||||
/**
|
||||
* 모든 창의 레이아웃 업데이트를 요청합니다.
|
||||
* 중복 실행을 방지하기 위해 isUpdating 플래그를 사용합니다.
|
||||
*/
|
||||
updateLayout() {
|
||||
if (this.isUpdating) return;
|
||||
this.isUpdating = true;
|
||||
|
||||
setImmediate(() => {
|
||||
this.positionWindows();
|
||||
this.isUpdating = false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 헤더 창을 기준으로 모든 기능 창들의 위치를 계산하고 배치합니다.
|
||||
*/
|
||||
positionWindows() {
|
||||
const header = this.windowPool.get('header');
|
||||
if (!header?.getBounds) return;
|
||||
|
||||
const headerBounds = header.getBounds();
|
||||
const display = getCurrentDisplay(header);
|
||||
const { width: screenWidth, height: screenHeight } = display.workAreaSize;
|
||||
const { x: workAreaX, y: workAreaY } = display.workArea;
|
||||
|
||||
const headerCenterX = headerBounds.x - workAreaX + headerBounds.width / 2;
|
||||
const headerCenterY = headerBounds.y - workAreaY + headerBounds.height / 2;
|
||||
|
||||
const relativeX = headerCenterX / screenWidth;
|
||||
const relativeY = headerCenterY / screenHeight;
|
||||
|
||||
const strategy = this.determineLayoutStrategy(headerBounds, screenWidth, screenHeight, relativeX, relativeY);
|
||||
|
||||
this.positionFeatureWindows(headerBounds, strategy, screenWidth, screenHeight, workAreaX, workAreaY);
|
||||
this.positionSettingsWindow(headerBounds, strategy, screenWidth, screenHeight, workAreaX, workAreaY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 헤더 창의 위치에 따라 기능 창들을 배치할 최적의 전략을 결정합니다.
|
||||
* @returns {{name: string, primary: string, secondary: string}} 레이아웃 전략
|
||||
*/
|
||||
determineLayoutStrategy(headerBounds, screenWidth, screenHeight, relativeX, relativeY) {
|
||||
const spaceBelow = screenHeight - (headerBounds.y + headerBounds.height);
|
||||
const spaceAbove = headerBounds.y;
|
||||
const spaceLeft = headerBounds.x;
|
||||
const spaceRight = screenWidth - (headerBounds.x + headerBounds.width);
|
||||
|
||||
if (spaceBelow >= 400) {
|
||||
return { name: 'below', primary: 'below', secondary: relativeX < 0.5 ? 'right' : 'left' };
|
||||
} else if (spaceAbove >= 400) {
|
||||
return { name: 'above', primary: 'above', secondary: relativeX < 0.5 ? 'right' : 'left' };
|
||||
} else if (relativeX < 0.3 && spaceRight >= 800) {
|
||||
return { name: 'right-side', primary: 'right', secondary: spaceBelow > spaceAbove ? 'below' : 'above' };
|
||||
} else if (relativeX > 0.7 && spaceLeft >= 800) {
|
||||
return { name: 'left-side', primary: 'left', secondary: spaceBelow > spaceAbove ? 'below' : 'above' };
|
||||
} else {
|
||||
return { name: 'adaptive', primary: spaceBelow > spaceAbove ? 'below' : 'above', secondary: spaceRight > spaceLeft ? 'right' : 'left' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 'ask'와 'listen' 창의 위치를 조정합니다.
|
||||
*/
|
||||
positionFeatureWindows(headerBounds, strategy, screenWidth, screenHeight, workAreaX, workAreaY) {
|
||||
const ask = this.windowPool.get('ask');
|
||||
const listen = this.windowPool.get('listen');
|
||||
const askVisible = ask && ask.isVisible() && !ask.isDestroyed();
|
||||
const listenVisible = listen && listen.isVisible() && !listen.isDestroyed();
|
||||
|
||||
if (!askVisible && !listenVisible) return;
|
||||
|
||||
const PAD = 8;
|
||||
const headerCenterXRel = headerBounds.x - workAreaX + headerBounds.width / 2;
|
||||
let askBounds = askVisible ? ask.getBounds() : null;
|
||||
let listenBounds = listenVisible ? listen.getBounds() : null;
|
||||
|
||||
if (askVisible && listenVisible) {
|
||||
const combinedWidth = listenBounds.width + PAD + askBounds.width;
|
||||
let groupStartXRel = headerCenterXRel - combinedWidth / 2;
|
||||
let listenXRel = groupStartXRel;
|
||||
let askXRel = groupStartXRel + listenBounds.width + PAD;
|
||||
|
||||
if (listenXRel < PAD) {
|
||||
listenXRel = PAD;
|
||||
askXRel = listenXRel + listenBounds.width + PAD;
|
||||
}
|
||||
if (askXRel + askBounds.width > screenWidth - PAD) {
|
||||
askXRel = screenWidth - PAD - askBounds.width;
|
||||
listenXRel = askXRel - listenBounds.width - PAD;
|
||||
}
|
||||
|
||||
let yRel = (strategy.primary === 'above')
|
||||
? headerBounds.y - workAreaY - Math.max(askBounds.height, listenBounds.height) - PAD
|
||||
: headerBounds.y - workAreaY + headerBounds.height + PAD;
|
||||
|
||||
listen.setBounds({ x: Math.round(listenXRel + workAreaX), y: Math.round(yRel + workAreaY), width: listenBounds.width, height: listenBounds.height });
|
||||
ask.setBounds({ x: Math.round(askXRel + workAreaX), y: Math.round(yRel + workAreaY), width: askBounds.width, height: askBounds.height });
|
||||
} else {
|
||||
const win = askVisible ? ask : listen;
|
||||
const winBounds = askVisible ? askBounds : listenBounds;
|
||||
let xRel = headerCenterXRel - winBounds.width / 2;
|
||||
let yRel = (strategy.primary === 'above')
|
||||
? headerBounds.y - workAreaY - winBounds.height - PAD
|
||||
: headerBounds.y - workAreaY + headerBounds.height + PAD;
|
||||
|
||||
xRel = Math.max(PAD, Math.min(screenWidth - winBounds.width - PAD, xRel));
|
||||
yRel = Math.max(PAD, Math.min(screenHeight - winBounds.height - PAD, yRel));
|
||||
|
||||
win.setBounds({ x: Math.round(xRel + workAreaX), y: Math.round(yRel + workAreaY), width: winBounds.width, height: winBounds.height });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 'settings' 창의 위치를 조정합니다.
|
||||
*/
|
||||
positionSettingsWindow(headerBounds, strategy, screenWidth, screenHeight, workAreaX, workAreaY) {
|
||||
const settings = this.windowPool.get('settings');
|
||||
if (!settings?.getBounds || !settings.isVisible()) return;
|
||||
|
||||
if (settings.__lockedByButton) {
|
||||
const headerDisplay = getCurrentDisplay(this.windowPool.get('header'));
|
||||
const settingsDisplay = getCurrentDisplay(settings);
|
||||
if (headerDisplay.id !== settingsDisplay.id) {
|
||||
settings.__lockedByButton = false;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const settingsBounds = settings.getBounds();
|
||||
const PAD = 5;
|
||||
const buttonPadding = 17;
|
||||
let x = headerBounds.x + headerBounds.width - settingsBounds.width - buttonPadding;
|
||||
let y = headerBounds.y + headerBounds.height + PAD;
|
||||
|
||||
const otherVisibleWindows = [];
|
||||
['listen', 'ask'].forEach(name => {
|
||||
const win = this.windowPool.get(name);
|
||||
if (win && win.isVisible() && !win.isDestroyed()) {
|
||||
otherVisibleWindows.push({ name, bounds: win.getBounds() });
|
||||
}
|
||||
});
|
||||
|
||||
const settingsNewBounds = { x, y, width: settingsBounds.width, height: settingsBounds.height };
|
||||
let hasOverlap = otherVisibleWindows.some(otherWin => this.boundsOverlap(settingsNewBounds, otherWin.bounds));
|
||||
|
||||
if (hasOverlap) {
|
||||
x = headerBounds.x + headerBounds.width + PAD;
|
||||
y = headerBounds.y;
|
||||
if (x + settingsBounds.width > screenWidth - 10) {
|
||||
x = headerBounds.x - settingsBounds.width - PAD;
|
||||
}
|
||||
if (x < 10) {
|
||||
x = headerBounds.x + headerBounds.width - settingsBounds.width - buttonPadding;
|
||||
y = headerBounds.y - settingsBounds.height - PAD;
|
||||
if (y < 10) {
|
||||
x = headerBounds.x + headerBounds.width - settingsBounds.width;
|
||||
y = headerBounds.y + headerBounds.height + PAD;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
x = Math.max(workAreaX + 10, Math.min(workAreaX + screenWidth - settingsBounds.width - 10, x));
|
||||
y = Math.max(workAreaY + 10, Math.min(workAreaY + screenHeight - settingsBounds.height - 10, y));
|
||||
|
||||
settings.setBounds({ x: Math.round(x), y: Math.round(y) });
|
||||
settings.moveTop();
|
||||
}
|
||||
|
||||
/**
|
||||
* 두 사각형 영역이 겹치는지 확인합니다.
|
||||
* @param {Rectangle} bounds1
|
||||
* @param {Rectangle} bounds2
|
||||
* @returns {boolean} 겹침 여부
|
||||
*/
|
||||
boundsOverlap(bounds1, bounds2) {
|
||||
const margin = 10;
|
||||
return !(
|
||||
bounds1.x + bounds1.width + margin < bounds2.x ||
|
||||
bounds2.x + bounds2.width + margin < bounds1.x ||
|
||||
bounds1.y + bounds1.height + margin < bounds2.y ||
|
||||
bounds2.y + bounds2.height + margin < bounds1.y
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WindowLayoutManager;
|
File diff suppressed because it is too large
Load Diff
@ -590,6 +590,43 @@ export class AskView extends LitElement {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-size: 14px;
|
||||
}
|
||||
:host-context(body.has-glass) .ask-container,
|
||||
:host-context(body.has-glass) .response-header,
|
||||
:host-context(body.has-glass) .response-icon,
|
||||
:host-context(body.has-glass) .copy-button,
|
||||
:host-context(body.has-glass) .close-button,
|
||||
:host-context(body.has-glass) .line-copy-button,
|
||||
:host-context(body.has-glass) .text-input-container,
|
||||
:host-context(body.has-glass) .response-container pre,
|
||||
:host-context(body.has-glass) .response-container p code,
|
||||
:host-context(body.has-glass) .response-container pre code {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
filter: none !important;
|
||||
backdrop-filter: none !important;
|
||||
}
|
||||
|
||||
/* ask-container 의 블러·그림자 레이어 제거 */
|
||||
:host-context(body.has-glass) .ask-container::before {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* hover/active 때 다시 생기는 배경도 차단 */
|
||||
:host-context(body.has-glass) .copy-button:hover,
|
||||
:host-context(body.has-glass) .close-button:hover,
|
||||
:host-context(body.has-glass) .line-copy-button,
|
||||
:host-context(body.has-glass) .line-copy-button:hover,
|
||||
:host-context(body.has-glass) .response-line:hover {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* 스크롤바 트랙·썸 마저 투명화 (원할 경우) */
|
||||
:host-context(body.has-glass) .response-container::-webkit-scrollbar-track,
|
||||
:host-context(body.has-glass) .response-container::-webkit-scrollbar-thumb {
|
||||
background: transparent !important;
|
||||
}
|
||||
`;
|
||||
|
||||
constructor() {
|
||||
@ -1378,9 +1415,9 @@ export class AskView extends LitElement {
|
||||
const responseHeight = responseEl.scrollHeight;
|
||||
const inputHeight = (inputEl && !inputEl.classList.contains('hidden')) ? inputEl.offsetHeight : 0;
|
||||
|
||||
const idealHeight = headerHeight + responseHeight + inputHeight + 20; // padding
|
||||
const idealHeight = headerHeight + responseHeight + inputHeight;
|
||||
|
||||
const targetHeight = Math.min(700, Math.max(200, idealHeight));
|
||||
const targetHeight = Math.min(700, idealHeight);
|
||||
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
ipcRenderer.invoke('adjust-window-height', targetHeight);
|
||||
|
@ -11,7 +11,7 @@ export class CustomizeView extends LitElement {
|
||||
:host {
|
||||
display: block;
|
||||
width: 180px;
|
||||
min-height: 180px;
|
||||
height: 100%;
|
||||
color: white;
|
||||
}
|
||||
|
||||
@ -234,6 +234,38 @@ export class CustomizeView extends LitElement {
|
||||
font-size: 11px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
:host-context(body.has-glass) .settings-container,
|
||||
:host-context(body.has-glass) .settings-button,
|
||||
:host-context(body.has-glass) .cmd-key,
|
||||
:host-context(body.has-glass) .shortcut-key,
|
||||
:host-context(body.has-glass) .api-key-section input {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
filter: none !important;
|
||||
backdrop-filter: none !important;
|
||||
}
|
||||
|
||||
/* 블러·그림자·gradient 레이어 제거 */
|
||||
:host-context(body.has-glass) .settings-container::before {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* hover/active 시 다시 생기는 배경도 차단 */
|
||||
:host-context(body.has-glass) .settings-button:hover,
|
||||
:host-context(body.has-glass) .shortcut-item:hover,
|
||||
:host-context(body.has-glass) .settings-button.danger:hover {
|
||||
background: transparent !important;
|
||||
border-color: transparent !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
/* 스크롤바 트랙·썸 투명화(선택 사항) */
|
||||
:host-context(body.has-glass) .settings-container::-webkit-scrollbar-track,
|
||||
:host-context(body.has-glass) .settings-container::-webkit-scrollbar-thumb {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
@ -390,9 +422,7 @@ export class CustomizeView extends LitElement {
|
||||
|
||||
updateScrollHeight() {
|
||||
const windowHeight = window.innerHeight;
|
||||
const headerHeight = 60;
|
||||
const padding = 40;
|
||||
const maxHeight = windowHeight - headerHeight - padding;
|
||||
const maxHeight = windowHeight;
|
||||
|
||||
this.style.maxHeight = `${maxHeight}px`;
|
||||
}
|
||||
|
@ -161,7 +161,7 @@ export class AssistantView extends LitElement {
|
||||
/* outline: 0.5px rgba(255, 255, 255, 0.5) solid; */
|
||||
/* outline-offset: -1px; */
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.assistant-container::after {
|
||||
@ -542,6 +542,73 @@ export class AssistantView extends LitElement {
|
||||
font-size: 10px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
:host-context(body.has-glass) .assistant-container,
|
||||
:host-context(body.has-glass) .top-bar,
|
||||
:host-context(body.has-glass) .toggle-button,
|
||||
:host-context(body.has-glass) .copy-button,
|
||||
:host-context(body.has-glass) .transcription-container,
|
||||
:host-context(body.has-glass) .insights-container,
|
||||
:host-context(body.has-glass) .stt-message,
|
||||
:host-context(body.has-glass) .outline-item,
|
||||
:host-context(body.has-glass) .request-item,
|
||||
:host-context(body.has-glass) .markdown-content,
|
||||
:host-context(body.has-glass) .insights-container pre,
|
||||
:host-context(body.has-glass) .insights-container p code,
|
||||
:host-context(body.has-glass) .insights-container pre code {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
filter: none !important;
|
||||
backdrop-filter: none !important;
|
||||
}
|
||||
|
||||
/* 가상 레이어·gradient 테두리 제거 */
|
||||
:host-context(body.has-glass) .assistant-container::before,
|
||||
:host-context(body.has-glass) .assistant-container::after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* hover 상태에서 생기는 배경도 차단 */
|
||||
:host-context(body.has-glass) .toggle-button:hover,
|
||||
:host-context(body.has-glass) .copy-button:hover,
|
||||
:host-context(body.has-glass) .outline-item:hover,
|
||||
:host-context(body.has-glass) .request-item.clickable:hover,
|
||||
:host-context(body.has-glass) .markdown-content:hover {
|
||||
background: transparent !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
/* 스크롤바 트랙·썸도 투명화(선택) */
|
||||
:host-context(body.has-glass) .transcription-container::-webkit-scrollbar-track,
|
||||
:host-context(body.has-glass) .transcription-container::-webkit-scrollbar-thumb,
|
||||
:host-context(body.has-glass) .insights-container::-webkit-scrollbar-track,
|
||||
:host-context(body.has-glass) .insights-container::-webkit-scrollbar-thumb {
|
||||
background: transparent !important;
|
||||
}
|
||||
:host-context(body.has-glass) * {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
transform: none !important;
|
||||
filter: none !important;
|
||||
backdrop-filter: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* 추가: 둥근 모서리와 스크롤바도 평면화하려면 */
|
||||
:host-context(body.has-glass) .assistant-container,
|
||||
:host-context(body.has-glass) .stt-message,
|
||||
:host-context(body.has-glass) .toggle-button,
|
||||
:host-context(body.has-glass) .copy-button {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
:host-context(body.has-glass) ::-webkit-scrollbar,
|
||||
:host-context(body.has-glass) ::-webkit-scrollbar-track,
|
||||
:host-context(body.has-glass) ::-webkit-scrollbar-thumb {
|
||||
background: transparent !important;
|
||||
width: 0 !important; /* 스크롤바 자체 숨기기 */
|
||||
}
|
||||
`;
|
||||
|
||||
static properties = {
|
||||
@ -777,9 +844,9 @@ export class AssistantView extends LitElement {
|
||||
|
||||
const contentHeight = activeContent.scrollHeight;
|
||||
|
||||
const idealHeight = topBarHeight + contentHeight + 20;
|
||||
const idealHeight = topBarHeight + contentHeight;
|
||||
|
||||
const targetHeight = Math.min(700, Math.max(200, idealHeight));
|
||||
const targetHeight = Math.min(700, idealHeight);
|
||||
|
||||
console.log(
|
||||
`[Height Adjusted] Mode: ${this.viewMode}, TopBar: ${topBarHeight}px, Content: ${contentHeight}px, Ideal: ${idealHeight}px, Target: ${targetHeight}px`
|
||||
|
Loading…
x
Reference in New Issue
Block a user