960 lines
26 KiB
TypeScript
960 lines
26 KiB
TypeScript
|
|
import {
|
||
|
|
customElement,
|
||
|
|
DeesElement,
|
||
|
|
type TemplateResult,
|
||
|
|
html,
|
||
|
|
property,
|
||
|
|
css,
|
||
|
|
cssManager,
|
||
|
|
state,
|
||
|
|
} from '@design.estate/dees-element';
|
||
|
|
import { DeesIcon } from '@design.estate/dees-catalog';
|
||
|
|
import { demo } from './eco-view-browser.demo.js';
|
||
|
|
|
||
|
|
// Ensure components are registered
|
||
|
|
DeesIcon;
|
||
|
|
|
||
|
|
declare global {
|
||
|
|
interface HTMLElementTagNameMap {
|
||
|
|
'eco-view-browser': EcoViewBrowser;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Types
|
||
|
|
export interface IBookmark {
|
||
|
|
id: string;
|
||
|
|
title: string;
|
||
|
|
url: string;
|
||
|
|
favicon?: string;
|
||
|
|
createdAt: Date;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface IBrowserState {
|
||
|
|
url: string;
|
||
|
|
title: string;
|
||
|
|
canGoBack: boolean;
|
||
|
|
canGoForward: boolean;
|
||
|
|
isLoading: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
export type TBrowserPanel = 'browser' | 'bookmarks';
|
||
|
|
|
||
|
|
@customElement('eco-view-browser')
|
||
|
|
export class EcoViewBrowser extends DeesElement {
|
||
|
|
public static demo = demo;
|
||
|
|
public static demoGroup = 'Views';
|
||
|
|
|
||
|
|
public static styles = [
|
||
|
|
cssManager.defaultStyles,
|
||
|
|
css`
|
||
|
|
:host {
|
||
|
|
display: block;
|
||
|
|
width: 100%;
|
||
|
|
height: 100%;
|
||
|
|
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 8%)')};
|
||
|
|
color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')};
|
||
|
|
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||
|
|
}
|
||
|
|
|
||
|
|
.browser-container {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
height: 100%;
|
||
|
|
}
|
||
|
|
|
||
|
|
.toolbar {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 8px;
|
||
|
|
padding: 8px 12px;
|
||
|
|
background: ${cssManager.bdTheme('#f5f5f7', 'hsl(240 6% 10%)')};
|
||
|
|
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 15%)')};
|
||
|
|
}
|
||
|
|
|
||
|
|
.nav-buttons {
|
||
|
|
display: flex;
|
||
|
|
gap: 4px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.nav-button {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
width: 32px;
|
||
|
|
height: 32px;
|
||
|
|
border: none;
|
||
|
|
border-radius: 6px;
|
||
|
|
background: transparent;
|
||
|
|
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 70%)')};
|
||
|
|
cursor: pointer;
|
||
|
|
transition: all 0.15s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.nav-button:hover:not(:disabled) {
|
||
|
|
background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')};
|
||
|
|
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
|
||
|
|
}
|
||
|
|
|
||
|
|
.nav-button:disabled {
|
||
|
|
opacity: 0.4;
|
||
|
|
cursor: not-allowed;
|
||
|
|
}
|
||
|
|
|
||
|
|
.nav-button.loading dees-icon {
|
||
|
|
animation: spin 1s linear infinite;
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes spin {
|
||
|
|
from { transform: rotate(0deg); }
|
||
|
|
to { transform: rotate(360deg); }
|
||
|
|
}
|
||
|
|
|
||
|
|
.url-container {
|
||
|
|
flex: 1;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 15%)')};
|
||
|
|
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(240 5% 25%)')};
|
||
|
|
border-radius: 8px;
|
||
|
|
padding: 0 12px;
|
||
|
|
height: 36px;
|
||
|
|
transition: border-color 0.15s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.url-container:focus-within {
|
||
|
|
border-color: hsl(217 91% 60%);
|
||
|
|
}
|
||
|
|
|
||
|
|
.url-icon {
|
||
|
|
margin-right: 8px;
|
||
|
|
opacity: 0.5;
|
||
|
|
}
|
||
|
|
|
||
|
|
.url-input {
|
||
|
|
flex: 1;
|
||
|
|
border: none;
|
||
|
|
background: transparent;
|
||
|
|
font-size: 14px;
|
||
|
|
color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 95%)')};
|
||
|
|
outline: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.url-input::placeholder {
|
||
|
|
color: ${cssManager.bdTheme('hsl(0 0% 60%)', 'hsl(0 0% 50%)')};
|
||
|
|
}
|
||
|
|
|
||
|
|
.bookmark-button {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
width: 32px;
|
||
|
|
height: 32px;
|
||
|
|
border: none;
|
||
|
|
border-radius: 6px;
|
||
|
|
background: transparent;
|
||
|
|
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
|
||
|
|
cursor: pointer;
|
||
|
|
transition: all 0.15s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.bookmark-button:hover {
|
||
|
|
background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')};
|
||
|
|
color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 80%)')};
|
||
|
|
}
|
||
|
|
|
||
|
|
.bookmark-button.bookmarked {
|
||
|
|
color: hsl(45 93% 47%);
|
||
|
|
}
|
||
|
|
|
||
|
|
.bookmark-button.bookmarked:hover {
|
||
|
|
color: hsl(45 93% 40%);
|
||
|
|
}
|
||
|
|
|
||
|
|
.menu-button {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
width: 32px;
|
||
|
|
height: 32px;
|
||
|
|
border: none;
|
||
|
|
border-radius: 6px;
|
||
|
|
background: transparent;
|
||
|
|
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
|
||
|
|
cursor: pointer;
|
||
|
|
transition: all 0.15s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.menu-button:hover {
|
||
|
|
background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')};
|
||
|
|
color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 80%)')};
|
||
|
|
}
|
||
|
|
|
||
|
|
.webview-container {
|
||
|
|
flex: 1;
|
||
|
|
position: relative;
|
||
|
|
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 5%)')};
|
||
|
|
overflow: hidden;
|
||
|
|
}
|
||
|
|
|
||
|
|
.webview-wrapper {
|
||
|
|
width: 100%;
|
||
|
|
height: 100%;
|
||
|
|
position: absolute;
|
||
|
|
top: 0;
|
||
|
|
left: 0;
|
||
|
|
right: 0;
|
||
|
|
bottom: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
webview {
|
||
|
|
width: 100%;
|
||
|
|
height: 100%;
|
||
|
|
border: none;
|
||
|
|
display: flex;
|
||
|
|
}
|
||
|
|
|
||
|
|
.placeholder {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
height: 100%;
|
||
|
|
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 50%)')};
|
||
|
|
gap: 16px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.placeholder dees-icon {
|
||
|
|
opacity: 0.3;
|
||
|
|
}
|
||
|
|
|
||
|
|
.placeholder-text {
|
||
|
|
font-size: 14px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.placeholder-hint {
|
||
|
|
font-size: 12px;
|
||
|
|
opacity: 0.7;
|
||
|
|
}
|
||
|
|
|
||
|
|
.bookmarks-bar {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 4px;
|
||
|
|
padding: 4px 12px;
|
||
|
|
background: ${cssManager.bdTheme('#fafafa', 'hsl(240 6% 9%)')};
|
||
|
|
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 92%)', 'hsl(240 5% 13%)')};
|
||
|
|
overflow-x: auto;
|
||
|
|
}
|
||
|
|
|
||
|
|
.bookmarks-bar::-webkit-scrollbar {
|
||
|
|
height: 4px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.bookmarks-bar::-webkit-scrollbar-track {
|
||
|
|
background: transparent;
|
||
|
|
}
|
||
|
|
|
||
|
|
.bookmarks-bar::-webkit-scrollbar-thumb {
|
||
|
|
background: ${cssManager.bdTheme('hsl(0 0% 80%)', 'hsl(240 5% 25%)')};
|
||
|
|
border-radius: 2px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.bookmark-chip {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 6px;
|
||
|
|
padding: 6px 10px;
|
||
|
|
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 15%)')};
|
||
|
|
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 20%)')};
|
||
|
|
border-radius: 6px;
|
||
|
|
font-size: 12px;
|
||
|
|
color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 80%)')};
|
||
|
|
cursor: pointer;
|
||
|
|
white-space: nowrap;
|
||
|
|
transition: all 0.15s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.bookmark-chip:hover {
|
||
|
|
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(240 5% 18%)')};
|
||
|
|
border-color: ${cssManager.bdTheme('hsl(0 0% 80%)', 'hsl(240 5% 25%)')};
|
||
|
|
}
|
||
|
|
|
||
|
|
.bookmark-chip .favicon {
|
||
|
|
width: 14px;
|
||
|
|
height: 14px;
|
||
|
|
border-radius: 2px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.dropdown-menu {
|
||
|
|
position: absolute;
|
||
|
|
top: 100%;
|
||
|
|
right: 0;
|
||
|
|
margin-top: 4px;
|
||
|
|
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 15%)')};
|
||
|
|
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 25%)')};
|
||
|
|
border-radius: 8px;
|
||
|
|
box-shadow: 0 4px 16px ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(0,0,0,0.3)')};
|
||
|
|
min-width: 180px;
|
||
|
|
z-index: 100;
|
||
|
|
overflow: hidden;
|
||
|
|
}
|
||
|
|
|
||
|
|
.dropdown-item {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 10px;
|
||
|
|
padding: 10px 14px;
|
||
|
|
font-size: 14px;
|
||
|
|
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
|
||
|
|
cursor: pointer;
|
||
|
|
transition: background 0.1s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.dropdown-item:hover {
|
||
|
|
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(240 5% 20%)')};
|
||
|
|
}
|
||
|
|
|
||
|
|
.dropdown-item.destructive {
|
||
|
|
color: hsl(0 72% 50%);
|
||
|
|
}
|
||
|
|
|
||
|
|
.dropdown-item.destructive:hover {
|
||
|
|
background: hsl(0 72% 95%);
|
||
|
|
}
|
||
|
|
|
||
|
|
.dropdown-divider {
|
||
|
|
height: 1px;
|
||
|
|
background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 20%)')};
|
||
|
|
margin: 4px 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.menu-container {
|
||
|
|
position: relative;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Bookmarks panel styles */
|
||
|
|
.bookmarks-panel {
|
||
|
|
padding: 32px 48px;
|
||
|
|
overflow-y: auto;
|
||
|
|
height: 100%;
|
||
|
|
}
|
||
|
|
|
||
|
|
.panel-header {
|
||
|
|
margin-bottom: 24px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.panel-title {
|
||
|
|
font-size: 28px;
|
||
|
|
font-weight: 600;
|
||
|
|
color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')};
|
||
|
|
margin: 0 0 8px 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.panel-description {
|
||
|
|
font-size: 14px;
|
||
|
|
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
|
||
|
|
margin: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.bookmarks-list {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
gap: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.bookmark-item {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 12px;
|
||
|
|
padding: 12px 16px;
|
||
|
|
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 12%)')};
|
||
|
|
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')};
|
||
|
|
border-radius: 8px;
|
||
|
|
cursor: pointer;
|
||
|
|
transition: all 0.15s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.bookmark-item:hover {
|
||
|
|
border-color: hsl(217 91% 60%);
|
||
|
|
transform: translateX(2px);
|
||
|
|
}
|
||
|
|
|
||
|
|
.bookmark-item .favicon {
|
||
|
|
width: 20px;
|
||
|
|
height: 20px;
|
||
|
|
border-radius: 4px;
|
||
|
|
flex-shrink: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.bookmark-info {
|
||
|
|
flex: 1;
|
||
|
|
min-width: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.bookmark-title {
|
||
|
|
font-size: 14px;
|
||
|
|
font-weight: 500;
|
||
|
|
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
|
||
|
|
white-space: nowrap;
|
||
|
|
overflow: hidden;
|
||
|
|
text-overflow: ellipsis;
|
||
|
|
}
|
||
|
|
|
||
|
|
.bookmark-url {
|
||
|
|
font-size: 12px;
|
||
|
|
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
|
||
|
|
white-space: nowrap;
|
||
|
|
overflow: hidden;
|
||
|
|
text-overflow: ellipsis;
|
||
|
|
}
|
||
|
|
|
||
|
|
.bookmark-delete {
|
||
|
|
opacity: 0;
|
||
|
|
transition: opacity 0.15s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.bookmark-item:hover .bookmark-delete {
|
||
|
|
opacity: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
.bookmark-delete-btn {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
width: 28px;
|
||
|
|
height: 28px;
|
||
|
|
border: none;
|
||
|
|
border-radius: 4px;
|
||
|
|
background: transparent;
|
||
|
|
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
|
||
|
|
cursor: pointer;
|
||
|
|
}
|
||
|
|
|
||
|
|
.bookmark-delete-btn:hover {
|
||
|
|
background: hsl(0 72% 95%);
|
||
|
|
color: hsl(0 72% 50%);
|
||
|
|
}
|
||
|
|
|
||
|
|
.empty-state {
|
||
|
|
text-align: center;
|
||
|
|
padding: 60px 20px;
|
||
|
|
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
|
||
|
|
}
|
||
|
|
|
||
|
|
.empty-state dees-icon {
|
||
|
|
margin-bottom: 16px;
|
||
|
|
opacity: 0.4;
|
||
|
|
}
|
||
|
|
|
||
|
|
.empty-state p {
|
||
|
|
font-size: 14px;
|
||
|
|
margin: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.loading-bar {
|
||
|
|
position: absolute;
|
||
|
|
top: 0;
|
||
|
|
left: 0;
|
||
|
|
right: 0;
|
||
|
|
height: 3px;
|
||
|
|
background: linear-gradient(90deg,
|
||
|
|
hsl(217 91% 60%) 0%,
|
||
|
|
hsl(217 91% 70%) 50%,
|
||
|
|
hsl(217 91% 60%) 100%
|
||
|
|
);
|
||
|
|
background-size: 200% 100%;
|
||
|
|
animation: loading 1.5s linear infinite;
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes loading {
|
||
|
|
0% { background-position: 200% 0; }
|
||
|
|
100% { background-position: -200% 0; }
|
||
|
|
}
|
||
|
|
`,
|
||
|
|
];
|
||
|
|
|
||
|
|
@property({ type: Array })
|
||
|
|
accessor bookmarks: IBookmark[] = [];
|
||
|
|
|
||
|
|
@property({ type: Boolean })
|
||
|
|
accessor showBookmarksBar = true;
|
||
|
|
|
||
|
|
@property({ type: String })
|
||
|
|
accessor defaultUrl = '';
|
||
|
|
|
||
|
|
@state()
|
||
|
|
accessor activePanel: TBrowserPanel = 'browser';
|
||
|
|
|
||
|
|
@state()
|
||
|
|
accessor currentUrl = '';
|
||
|
|
|
||
|
|
@state()
|
||
|
|
accessor inputUrl = '';
|
||
|
|
|
||
|
|
@state()
|
||
|
|
accessor pageTitle = '';
|
||
|
|
|
||
|
|
@state()
|
||
|
|
accessor canGoBack = false;
|
||
|
|
|
||
|
|
@state()
|
||
|
|
accessor canGoForward = false;
|
||
|
|
|
||
|
|
@state()
|
||
|
|
accessor isLoading = false;
|
||
|
|
|
||
|
|
@state()
|
||
|
|
accessor showMenu = false;
|
||
|
|
|
||
|
|
private isElectron = false;
|
||
|
|
private webviewEventsBound = false;
|
||
|
|
|
||
|
|
async connectedCallback(): Promise<void> {
|
||
|
|
await super.connectedCallback();
|
||
|
|
// Check if we're in Electron - webview tag only works in Electron
|
||
|
|
// Since nodeIntegration is disabled, we check for the existence of electronAPI
|
||
|
|
// or if we're in a file:// protocol (Electron loads from file://)
|
||
|
|
this.isElectron = typeof window !== 'undefined' && (
|
||
|
|
typeof (window as any).electronAPI !== 'undefined' ||
|
||
|
|
window.location.protocol === 'file:' ||
|
||
|
|
navigator.userAgent.includes('Electron')
|
||
|
|
);
|
||
|
|
|
||
|
|
if (this.defaultUrl) {
|
||
|
|
this.inputUrl = this.defaultUrl;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
protected updated(changedProperties: Map<string, any>): void {
|
||
|
|
super.updated(changedProperties);
|
||
|
|
// Bind webview events after it's rendered
|
||
|
|
this.bindWebviewEvents();
|
||
|
|
}
|
||
|
|
|
||
|
|
private bindWebviewEvents(): void {
|
||
|
|
if (!this.isElectron || this.webviewEventsBound) return;
|
||
|
|
|
||
|
|
const webview = this.shadowRoot?.querySelector('webview') as any;
|
||
|
|
if (!webview) return;
|
||
|
|
|
||
|
|
this.webviewEventsBound = true;
|
||
|
|
|
||
|
|
webview.addEventListener('did-start-loading', () => {
|
||
|
|
this.isLoading = true;
|
||
|
|
});
|
||
|
|
|
||
|
|
webview.addEventListener('did-stop-loading', () => {
|
||
|
|
this.isLoading = false;
|
||
|
|
this.updateNavigationState();
|
||
|
|
});
|
||
|
|
|
||
|
|
webview.addEventListener('did-navigate', (e: any) => {
|
||
|
|
this.currentUrl = e.url || this.currentUrl;
|
||
|
|
this.inputUrl = this.currentUrl;
|
||
|
|
this.updateNavigationState();
|
||
|
|
});
|
||
|
|
|
||
|
|
webview.addEventListener('did-navigate-in-page', (e: any) => {
|
||
|
|
if (e.isMainFrame) {
|
||
|
|
this.currentUrl = e.url || this.currentUrl;
|
||
|
|
this.inputUrl = this.currentUrl;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
webview.addEventListener('page-title-updated', (e: any) => {
|
||
|
|
this.pageTitle = e.title || '';
|
||
|
|
});
|
||
|
|
|
||
|
|
webview.addEventListener('page-favicon-updated', (e: any) => {
|
||
|
|
// Could store favicon for bookmarks
|
||
|
|
});
|
||
|
|
|
||
|
|
webview.addEventListener('dom-ready', () => {
|
||
|
|
this.updateNavigationState();
|
||
|
|
});
|
||
|
|
|
||
|
|
console.log('Webview events bound');
|
||
|
|
}
|
||
|
|
|
||
|
|
public render(): TemplateResult {
|
||
|
|
return html`
|
||
|
|
<div class="browser-container">
|
||
|
|
${this.renderToolbar()}
|
||
|
|
${this.showBookmarksBar && this.bookmarks.length > 0 ? this.renderBookmarksBar() : ''}
|
||
|
|
${this.activePanel === 'browser' ? this.renderBrowserContent() : this.renderBookmarksPanel()}
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
private renderToolbar(): TemplateResult {
|
||
|
|
return html`
|
||
|
|
<div class="toolbar">
|
||
|
|
<div class="nav-buttons">
|
||
|
|
<button
|
||
|
|
class="nav-button"
|
||
|
|
title="Back"
|
||
|
|
?disabled=${!this.canGoBack}
|
||
|
|
@click=${this.handleBack}
|
||
|
|
>
|
||
|
|
<dees-icon .icon=${'lucide:arrowLeft'} .iconSize=${18}></dees-icon>
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
class="nav-button"
|
||
|
|
title="Forward"
|
||
|
|
?disabled=${!this.canGoForward}
|
||
|
|
@click=${this.handleForward}
|
||
|
|
>
|
||
|
|
<dees-icon .icon=${'lucide:arrowRight'} .iconSize=${18}></dees-icon>
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
class="nav-button ${this.isLoading ? 'loading' : ''}"
|
||
|
|
title=${this.isLoading ? 'Stop' : 'Refresh'}
|
||
|
|
@click=${this.isLoading ? this.handleStop : this.handleRefresh}
|
||
|
|
>
|
||
|
|
<dees-icon
|
||
|
|
.icon=${this.isLoading ? 'lucide:loader' : 'lucide:refreshCw'}
|
||
|
|
.iconSize=${18}
|
||
|
|
></dees-icon>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="url-container">
|
||
|
|
<dees-icon class="url-icon" .icon=${'lucide:globe'} .iconSize=${16}></dees-icon>
|
||
|
|
<input
|
||
|
|
class="url-input"
|
||
|
|
type="text"
|
||
|
|
placeholder="Enter URL or search..."
|
||
|
|
.value=${this.inputUrl}
|
||
|
|
@input=${(e: InputEvent) => this.inputUrl = (e.target as HTMLInputElement).value}
|
||
|
|
@keydown=${this.handleUrlKeydown}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<button
|
||
|
|
class="bookmark-button ${this.isCurrentPageBookmarked() ? 'bookmarked' : ''}"
|
||
|
|
title=${this.isCurrentPageBookmarked() ? 'Remove bookmark' : 'Add bookmark'}
|
||
|
|
@click=${this.toggleBookmark}
|
||
|
|
?disabled=${!this.currentUrl}
|
||
|
|
>
|
||
|
|
<dees-icon
|
||
|
|
.icon=${this.isCurrentPageBookmarked() ? 'lucide:star' : 'lucide:star'}
|
||
|
|
.iconSize=${18}
|
||
|
|
></dees-icon>
|
||
|
|
</button>
|
||
|
|
|
||
|
|
<div class="menu-container">
|
||
|
|
<button
|
||
|
|
class="menu-button"
|
||
|
|
title="Menu"
|
||
|
|
@click=${() => this.showMenu = !this.showMenu}
|
||
|
|
>
|
||
|
|
<dees-icon .icon=${'lucide:moreVertical'} .iconSize=${18}></dees-icon>
|
||
|
|
</button>
|
||
|
|
${this.showMenu ? this.renderMenu() : ''}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
private renderMenu(): TemplateResult {
|
||
|
|
return html`
|
||
|
|
<div class="dropdown-menu" @mouseleave=${() => this.showMenu = false}>
|
||
|
|
<div class="dropdown-item" @click=${() => { this.activePanel = 'bookmarks'; this.showMenu = false; }}>
|
||
|
|
<dees-icon .icon=${'lucide:bookmark'} .iconSize=${18}></dees-icon>
|
||
|
|
Bookmarks
|
||
|
|
</div>
|
||
|
|
<div class="dropdown-item" @click=${this.handleNewTab}>
|
||
|
|
<dees-icon .icon=${'lucide:plus'} .iconSize=${18}></dees-icon>
|
||
|
|
New Tab
|
||
|
|
</div>
|
||
|
|
<div class="dropdown-divider"></div>
|
||
|
|
<div class="dropdown-item" @click=${this.handleOpenDevTools}>
|
||
|
|
<dees-icon .icon=${'lucide:code'} .iconSize=${18}></dees-icon>
|
||
|
|
Developer Tools
|
||
|
|
</div>
|
||
|
|
<div class="dropdown-divider"></div>
|
||
|
|
<div class="dropdown-item" @click=${this.handleCopyUrl}>
|
||
|
|
<dees-icon .icon=${'lucide:copy'} .iconSize=${18}></dees-icon>
|
||
|
|
Copy URL
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
private renderBookmarksBar(): TemplateResult {
|
||
|
|
return html`
|
||
|
|
<div class="bookmarks-bar">
|
||
|
|
${this.bookmarks.slice(0, 10).map(bookmark => html`
|
||
|
|
<div
|
||
|
|
class="bookmark-chip"
|
||
|
|
title=${bookmark.url}
|
||
|
|
@click=${() => this.navigate(bookmark.url)}
|
||
|
|
>
|
||
|
|
${bookmark.favicon
|
||
|
|
? html`<img class="favicon" src=${bookmark.favicon} alt="" />`
|
||
|
|
: html`<dees-icon .icon=${'lucide:globe'} .iconSize=${14}></dees-icon>`
|
||
|
|
}
|
||
|
|
${bookmark.title || new URL(bookmark.url).hostname}
|
||
|
|
</div>
|
||
|
|
`)}
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
private renderBrowserContent(): TemplateResult {
|
||
|
|
return html`
|
||
|
|
<div class="webview-container">
|
||
|
|
${this.isLoading ? html`<div class="loading-bar"></div>` : ''}
|
||
|
|
<div class="webview-wrapper">
|
||
|
|
${this.currentUrl ? this.renderWebview() : this.renderPlaceholder()}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
private renderWebview(): TemplateResult {
|
||
|
|
// In Electron, we use webview tag. In browser, we show a notice.
|
||
|
|
if (this.isElectron) {
|
||
|
|
// Reset event binding flag when URL changes so events get rebound
|
||
|
|
// Note: webview events are bound in updated() lifecycle
|
||
|
|
return html`
|
||
|
|
<webview
|
||
|
|
src=${this.currentUrl}
|
||
|
|
style="width: 100%; height: 100%;"
|
||
|
|
allowpopups
|
||
|
|
></webview>
|
||
|
|
`;
|
||
|
|
} else {
|
||
|
|
// Fallback for non-Electron environment (demo/testing)
|
||
|
|
return html`
|
||
|
|
<div class="placeholder">
|
||
|
|
<dees-icon .icon=${'lucide:monitor'} .iconSize=${48}></dees-icon>
|
||
|
|
<span class="placeholder-text">Webview requires Electron environment</span>
|
||
|
|
<span class="placeholder-hint">URL: ${this.currentUrl}</span>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private renderPlaceholder(): TemplateResult {
|
||
|
|
return html`
|
||
|
|
<div class="placeholder">
|
||
|
|
<dees-icon .icon=${'lucide:globe'} .iconSize=${64}></dees-icon>
|
||
|
|
<span class="placeholder-text">Enter a URL to start browsing</span>
|
||
|
|
<span class="placeholder-hint">or select a bookmark</span>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
private renderBookmarksPanel(): TemplateResult {
|
||
|
|
return html`
|
||
|
|
<div class="bookmarks-panel">
|
||
|
|
<div class="panel-header">
|
||
|
|
<h2 class="panel-title">Bookmarks</h2>
|
||
|
|
<p class="panel-description">Your saved websites</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
${this.bookmarks.length > 0
|
||
|
|
? html`
|
||
|
|
<div class="bookmarks-list">
|
||
|
|
${this.bookmarks.map(bookmark => html`
|
||
|
|
<div class="bookmark-item" @click=${() => this.handleBookmarkClick(bookmark)}>
|
||
|
|
${bookmark.favicon
|
||
|
|
? html`<img class="favicon" src=${bookmark.favicon} alt="" />`
|
||
|
|
: html`<dees-icon .icon=${'lucide:globe'} .iconSize=${20}></dees-icon>`
|
||
|
|
}
|
||
|
|
<div class="bookmark-info">
|
||
|
|
<div class="bookmark-title">${bookmark.title || bookmark.url}</div>
|
||
|
|
<div class="bookmark-url">${bookmark.url}</div>
|
||
|
|
</div>
|
||
|
|
<div class="bookmark-delete">
|
||
|
|
<button
|
||
|
|
class="bookmark-delete-btn"
|
||
|
|
title="Delete bookmark"
|
||
|
|
@click=${(e: Event) => { e.stopPropagation(); this.removeBookmark(bookmark.id); }}
|
||
|
|
>
|
||
|
|
<dees-icon .icon=${'lucide:trash2'} .iconSize=${16}></dees-icon>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`)}
|
||
|
|
</div>
|
||
|
|
`
|
||
|
|
: html`
|
||
|
|
<div class="empty-state">
|
||
|
|
<dees-icon .icon=${'lucide:bookmark'} .iconSize=${48}></dees-icon>
|
||
|
|
<p>No bookmarks yet. Star a page to save it here.</p>
|
||
|
|
</div>
|
||
|
|
`
|
||
|
|
}
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Navigation methods
|
||
|
|
private navigate(url: string): void {
|
||
|
|
if (!url) return;
|
||
|
|
|
||
|
|
// Add protocol if missing
|
||
|
|
if (!/^https?:\/\//i.test(url)) {
|
||
|
|
// Check if it looks like a domain
|
||
|
|
if (/^[a-zA-Z0-9][a-zA-Z0-9-]*\.[a-zA-Z]{2,}/.test(url)) {
|
||
|
|
url = 'https://' + url;
|
||
|
|
} else {
|
||
|
|
// Treat as search query
|
||
|
|
url = `https://www.google.com/search?q=${encodeURIComponent(url)}`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
this.currentUrl = url;
|
||
|
|
this.inputUrl = url;
|
||
|
|
this.isLoading = true;
|
||
|
|
|
||
|
|
this.dispatchEvent(new CustomEvent('navigate', {
|
||
|
|
detail: { url },
|
||
|
|
bubbles: true,
|
||
|
|
composed: true,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleUrlKeydown(e: KeyboardEvent): void {
|
||
|
|
if (e.key === 'Enter') {
|
||
|
|
this.navigate(this.inputUrl);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private get webview(): any {
|
||
|
|
return this.shadowRoot?.querySelector('webview');
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleBack(): void {
|
||
|
|
const wv = this.webview;
|
||
|
|
if (wv && this.isElectron) {
|
||
|
|
wv.goBack();
|
||
|
|
}
|
||
|
|
this.dispatchEvent(new CustomEvent('browser-back', { bubbles: true, composed: true }));
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleForward(): void {
|
||
|
|
const wv = this.webview;
|
||
|
|
if (wv && this.isElectron) {
|
||
|
|
wv.goForward();
|
||
|
|
}
|
||
|
|
this.dispatchEvent(new CustomEvent('browser-forward', { bubbles: true, composed: true }));
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleRefresh(): void {
|
||
|
|
const wv = this.webview;
|
||
|
|
if (wv && this.isElectron) {
|
||
|
|
wv.reload();
|
||
|
|
}
|
||
|
|
this.dispatchEvent(new CustomEvent('browser-refresh', { bubbles: true, composed: true }));
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleStop(): void {
|
||
|
|
const wv = this.webview;
|
||
|
|
if (wv && this.isElectron) {
|
||
|
|
wv.stop();
|
||
|
|
}
|
||
|
|
this.isLoading = false;
|
||
|
|
this.dispatchEvent(new CustomEvent('browser-stop', { bubbles: true, composed: true }));
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleFaviconUpdate(e: any): void {
|
||
|
|
// Could update current page favicon for bookmarks
|
||
|
|
}
|
||
|
|
|
||
|
|
private updateNavigationState(): void {
|
||
|
|
if (this.webview && this.isElectron) {
|
||
|
|
this.canGoBack = (this.webview as any).canGoBack?.() || false;
|
||
|
|
this.canGoForward = (this.webview as any).canGoForward?.() || false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Bookmark methods
|
||
|
|
private isCurrentPageBookmarked(): boolean {
|
||
|
|
return this.bookmarks.some(b => b.url === this.currentUrl);
|
||
|
|
}
|
||
|
|
|
||
|
|
private toggleBookmark(): void {
|
||
|
|
if (!this.currentUrl) return;
|
||
|
|
|
||
|
|
if (this.isCurrentPageBookmarked()) {
|
||
|
|
const bookmark = this.bookmarks.find(b => b.url === this.currentUrl);
|
||
|
|
if (bookmark) {
|
||
|
|
this.removeBookmark(bookmark.id);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
this.addBookmark();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private addBookmark(): void {
|
||
|
|
const bookmark: IBookmark = {
|
||
|
|
id: crypto.randomUUID(),
|
||
|
|
title: this.pageTitle || this.currentUrl,
|
||
|
|
url: this.currentUrl,
|
||
|
|
createdAt: new Date(),
|
||
|
|
};
|
||
|
|
|
||
|
|
this.dispatchEvent(new CustomEvent('bookmark-add', {
|
||
|
|
detail: { bookmark },
|
||
|
|
bubbles: true,
|
||
|
|
composed: true,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
private removeBookmark(id: string): void {
|
||
|
|
this.dispatchEvent(new CustomEvent('bookmark-remove', {
|
||
|
|
detail: { id },
|
||
|
|
bubbles: true,
|
||
|
|
composed: true,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleBookmarkClick(bookmark: IBookmark): void {
|
||
|
|
this.navigate(bookmark.url);
|
||
|
|
this.activePanel = 'browser';
|
||
|
|
}
|
||
|
|
|
||
|
|
// Menu actions
|
||
|
|
private handleNewTab(): void {
|
||
|
|
this.showMenu = false;
|
||
|
|
this.dispatchEvent(new CustomEvent('new-tab', { bubbles: true, composed: true }));
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleOpenDevTools(): void {
|
||
|
|
this.showMenu = false;
|
||
|
|
if (this.webview && this.isElectron) {
|
||
|
|
(this.webview as any).openDevTools();
|
||
|
|
}
|
||
|
|
this.dispatchEvent(new CustomEvent('open-devtools', { bubbles: true, composed: true }));
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleCopyUrl(): void {
|
||
|
|
this.showMenu = false;
|
||
|
|
if (this.currentUrl) {
|
||
|
|
navigator.clipboard.writeText(this.currentUrl);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Public API
|
||
|
|
public goTo(url: string): void {
|
||
|
|
this.navigate(url);
|
||
|
|
}
|
||
|
|
|
||
|
|
public setBookmarks(bookmarks: IBookmark[]): void {
|
||
|
|
this.bookmarks = bookmarks;
|
||
|
|
}
|
||
|
|
|
||
|
|
public getState(): IBrowserState {
|
||
|
|
return {
|
||
|
|
url: this.currentUrl,
|
||
|
|
title: this.pageTitle,
|
||
|
|
canGoBack: this.canGoBack,
|
||
|
|
canGoForward: this.canGoForward,
|
||
|
|
isLoading: this.isLoading,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|