840 lines
23 KiB
TypeScript
840 lines
23 KiB
TypeScript
import {
|
|
customElement,
|
|
DeesElement,
|
|
type TemplateResult,
|
|
html,
|
|
property,
|
|
css,
|
|
cssManager,
|
|
state,
|
|
} from '@design.estate/dees-element';
|
|
import { DeesAppuiSecondarymenu, DeesIcon } from '@design.estate/dees-catalog';
|
|
import type { ISecondaryMenuGroup } from '../../elements/interfaces/secondarymenu.js';
|
|
import { demo } from './eco-view-scan.demo.js';
|
|
|
|
// Ensure components are registered
|
|
DeesAppuiSecondarymenu;
|
|
DeesIcon;
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
'eco-view-scan': EcoViewScan;
|
|
}
|
|
}
|
|
|
|
// Types
|
|
export type TScanFormat = 'pdf' | 'jpeg' | 'png' | 'tiff';
|
|
export type TScanColorMode = 'color' | 'grayscale' | 'blackwhite';
|
|
export type TScanSource = 'flatbed' | 'adf' | 'adf-duplex';
|
|
export type TScanPanel = 'scan' | 'history' | 'settings';
|
|
|
|
export interface IScanSettings {
|
|
format: TScanFormat;
|
|
resolution: number;
|
|
colorMode: TScanColorMode;
|
|
source: TScanSource;
|
|
}
|
|
|
|
export interface IScannedDocument {
|
|
id: string;
|
|
timestamp: Date;
|
|
format: TScanFormat;
|
|
data: string; // base64
|
|
thumbnail?: string;
|
|
size: number;
|
|
name?: string;
|
|
}
|
|
|
|
export interface IScannerInfo {
|
|
id: string;
|
|
name: string;
|
|
address: string;
|
|
status: 'online' | 'offline' | 'busy' | 'error';
|
|
capabilities?: {
|
|
resolutions: number[];
|
|
formats: TScanFormat[];
|
|
colorModes: TScanColorMode[];
|
|
sources: TScanSource[];
|
|
};
|
|
}
|
|
|
|
export interface IDataProviderInfo {
|
|
id: string;
|
|
name: string;
|
|
icon?: string;
|
|
}
|
|
|
|
@customElement('eco-view-scan')
|
|
export class EcoViewScan 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('#f5f5f7', 'hsl(240 6% 10%)')};
|
|
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;
|
|
}
|
|
|
|
.scan-container {
|
|
display: flex;
|
|
height: 100%;
|
|
}
|
|
|
|
dees-appui-secondarymenu {
|
|
flex-shrink: 0;
|
|
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 8%)')};
|
|
border-right: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 15%)')};
|
|
}
|
|
|
|
.content {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 32px 48px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.panel-header {
|
|
margin-bottom: 24px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.panel-header-left {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
.panel-title {
|
|
font-size: 28px;
|
|
font-weight: 600;
|
|
color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')};
|
|
}
|
|
|
|
.panel-description {
|
|
font-size: 14px;
|
|
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
|
|
}
|
|
|
|
.scanner-selector {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.scanner-selector label {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 70%)')};
|
|
}
|
|
|
|
.scanner-selector select {
|
|
flex: 1;
|
|
max-width: 300px;
|
|
padding: 10px 14px;
|
|
font-size: 14px;
|
|
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(240 5% 25%)')};
|
|
border-radius: 8px;
|
|
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 15%)')};
|
|
color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 95%)')};
|
|
cursor: pointer;
|
|
}
|
|
|
|
.scanner-selector select:focus {
|
|
outline: none;
|
|
border-color: hsl(217 91% 60%);
|
|
}
|
|
|
|
.preview-area {
|
|
flex: 1;
|
|
min-height: 300px;
|
|
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 12%)')};
|
|
border: 2px dashed ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(240 5% 25%)')};
|
|
border-radius: 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
overflow: hidden;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.preview-area.has-image {
|
|
border-style: solid;
|
|
}
|
|
|
|
.preview-placeholder {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 12px;
|
|
color: ${cssManager.bdTheme('hsl(0 0% 60%)', 'hsl(0 0% 50%)')};
|
|
}
|
|
|
|
.preview-placeholder dees-icon {
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.preview-image {
|
|
max-width: 100%;
|
|
max-height: 100%;
|
|
object-fit: contain;
|
|
}
|
|
|
|
.action-bar {
|
|
display: flex;
|
|
gap: 12px;
|
|
margin-bottom: 32px;
|
|
}
|
|
|
|
.action-button {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 12px 24px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
border: none;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: all 0.15s ease;
|
|
}
|
|
|
|
.action-button.primary {
|
|
background: hsl(217 91% 60%);
|
|
color: white;
|
|
}
|
|
|
|
.action-button.primary:hover:not(:disabled) {
|
|
background: hsl(217 91% 55%);
|
|
}
|
|
|
|
.action-button.secondary {
|
|
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(240 5% 20%)')};
|
|
color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 90%)')};
|
|
}
|
|
|
|
.action-button.secondary:hover:not(:disabled) {
|
|
background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 25%)')};
|
|
}
|
|
|
|
.action-button:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.action-button.scanning dees-icon {
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
from { transform: rotate(0deg); }
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
.dropdown-container {
|
|
position: relative;
|
|
}
|
|
|
|
.dropdown-menu {
|
|
position: absolute;
|
|
bottom: 100%;
|
|
left: 0;
|
|
margin-bottom: 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: 200px;
|
|
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 dees-icon {
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.settings-section {
|
|
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 12%)')};
|
|
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')};
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.settings-section-title {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
margin-bottom: 16px;
|
|
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
|
|
}
|
|
|
|
.setting-row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 12px 0;
|
|
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 92%)', 'hsl(240 5% 18%)')};
|
|
}
|
|
|
|
.setting-row:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.setting-label {
|
|
font-size: 14px;
|
|
color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 80%)')};
|
|
}
|
|
|
|
.setting-control select {
|
|
padding: 8px 12px;
|
|
font-size: 14px;
|
|
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(240 5% 25%)')};
|
|
border-radius: 6px;
|
|
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 15%)')};
|
|
color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 95%)')};
|
|
cursor: pointer;
|
|
}
|
|
|
|
.history-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
|
gap: 16px;
|
|
}
|
|
|
|
.history-item {
|
|
background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 12%)')};
|
|
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')};
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
cursor: pointer;
|
|
transition: all 0.15s ease;
|
|
}
|
|
|
|
.history-item:hover {
|
|
border-color: hsl(217 91% 60%);
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(0,0,0,0.3)')};
|
|
}
|
|
|
|
.history-thumbnail {
|
|
width: 100%;
|
|
aspect-ratio: 1;
|
|
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(240 5% 18%)')};
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.history-thumbnail img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.history-info {
|
|
padding: 10px;
|
|
}
|
|
|
|
.history-name {
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
|
|
margin-bottom: 4px;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.history-date {
|
|
font-size: 12px;
|
|
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
|
|
}
|
|
|
|
.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;
|
|
}
|
|
`,
|
|
];
|
|
|
|
@property({ type: Array })
|
|
accessor scanners: IScannerInfo[] = [];
|
|
|
|
@property({ type: Array })
|
|
accessor providers: IDataProviderInfo[] = [];
|
|
|
|
@state()
|
|
accessor activePanel: TScanPanel = 'scan';
|
|
|
|
@state()
|
|
accessor selectedScannerId: string | null = null;
|
|
|
|
@state()
|
|
accessor isScanning = false;
|
|
|
|
@state()
|
|
accessor currentPreview: string | null = null;
|
|
|
|
@state()
|
|
accessor currentDocument: IScannedDocument | null = null;
|
|
|
|
@state()
|
|
accessor scanHistory: IScannedDocument[] = [];
|
|
|
|
@state()
|
|
accessor settings: IScanSettings = {
|
|
format: 'pdf',
|
|
resolution: 300,
|
|
colorMode: 'color',
|
|
source: 'flatbed',
|
|
};
|
|
|
|
@state()
|
|
accessor showSendToMenu = false;
|
|
|
|
private get selectedScanner(): IScannerInfo | null {
|
|
return this.scanners.find(s => s.id === this.selectedScannerId) || null;
|
|
}
|
|
|
|
private get availableResolutions(): number[] {
|
|
return this.selectedScanner?.capabilities?.resolutions || [150, 300, 600];
|
|
}
|
|
|
|
private get availableFormats(): TScanFormat[] {
|
|
return this.selectedScanner?.capabilities?.formats || ['pdf', 'jpeg', 'png'];
|
|
}
|
|
|
|
private get availableColorModes(): TScanColorMode[] {
|
|
return this.selectedScanner?.capabilities?.colorModes || ['color', 'grayscale'];
|
|
}
|
|
|
|
private get availableSources(): TScanSource[] {
|
|
return this.selectedScanner?.capabilities?.sources || ['flatbed'];
|
|
}
|
|
|
|
public render(): TemplateResult {
|
|
return html`
|
|
<div class="scan-container">
|
|
<dees-appui-secondarymenu
|
|
.menuGroups=${this.getMenuGroups()}
|
|
.selectedKey=${this.activePanel}
|
|
></dees-appui-secondarymenu>
|
|
<div class="content">
|
|
${this.renderContent()}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private getMenuGroups(): ISecondaryMenuGroup[] {
|
|
return [
|
|
{
|
|
name: 'SCAN',
|
|
iconName: 'lucide:scan',
|
|
items: [
|
|
{
|
|
key: 'scan',
|
|
label: 'New Scan',
|
|
iconName: 'lucide:plus',
|
|
action: () => this.activePanel = 'scan',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: 'HISTORY',
|
|
iconName: 'lucide:history',
|
|
items: [
|
|
{
|
|
key: 'history',
|
|
label: 'Recent Scans',
|
|
iconName: 'lucide:clock',
|
|
action: () => this.activePanel = 'history',
|
|
badge: this.scanHistory.length > 0 ? this.scanHistory.length : undefined,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: 'OPTIONS',
|
|
iconName: 'lucide:settings',
|
|
items: [
|
|
{
|
|
key: 'settings',
|
|
label: 'Scan Settings',
|
|
iconName: 'lucide:sliders',
|
|
action: () => this.activePanel = 'settings',
|
|
},
|
|
],
|
|
},
|
|
];
|
|
}
|
|
|
|
private renderContent(): TemplateResult {
|
|
switch (this.activePanel) {
|
|
case 'scan':
|
|
return this.renderScanPanel();
|
|
case 'history':
|
|
return this.renderHistoryPanel();
|
|
case 'settings':
|
|
return this.renderSettingsPanel();
|
|
default:
|
|
return this.renderScanPanel();
|
|
}
|
|
}
|
|
|
|
private renderScanPanel(): TemplateResult {
|
|
return html`
|
|
<div class="panel-header">
|
|
<div class="panel-header-left">
|
|
<h2 class="panel-title">Scan Document</h2>
|
|
<p class="panel-description">Select a scanner and start scanning</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="scanner-selector">
|
|
<label>Scanner:</label>
|
|
<select
|
|
.value=${this.selectedScannerId || ''}
|
|
@change=${(e: Event) => this.handleScannerChange(e)}
|
|
>
|
|
<option value="">Select a scanner...</option>
|
|
${this.scanners.map(scanner => html`
|
|
<option value=${scanner.id} ?selected=${scanner.id === this.selectedScannerId}>
|
|
${scanner.name} ${scanner.status !== 'online' ? `(${scanner.status})` : ''}
|
|
</option>
|
|
`)}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="preview-area ${this.currentPreview ? 'has-image' : ''}">
|
|
${this.currentPreview
|
|
? html`<img class="preview-image" src=${this.currentPreview} alt="Scan preview" />`
|
|
: html`
|
|
<div class="preview-placeholder">
|
|
<dees-icon .icon=${'lucide:scan'} .iconSize=${48}></dees-icon>
|
|
<span>Scan preview will appear here</span>
|
|
</div>
|
|
`
|
|
}
|
|
</div>
|
|
|
|
<div class="action-bar">
|
|
<button
|
|
class="action-button primary ${this.isScanning ? 'scanning' : ''}"
|
|
?disabled=${!this.selectedScannerId || this.isScanning}
|
|
@click=${this.handleScan}
|
|
>
|
|
<dees-icon .icon=${this.isScanning ? 'lucide:loader' : 'lucide:scan'} .iconSize=${18}></dees-icon>
|
|
${this.isScanning ? 'Scanning...' : 'Scan'}
|
|
</button>
|
|
|
|
<div class="dropdown-container">
|
|
<button
|
|
class="action-button secondary"
|
|
?disabled=${!this.currentDocument}
|
|
@click=${() => this.showSendToMenu = !this.showSendToMenu}
|
|
>
|
|
<dees-icon .icon=${'lucide:send'} .iconSize=${18}></dees-icon>
|
|
Send To
|
|
<dees-icon .icon=${'lucide:chevronUp'} .iconSize=${14}></dees-icon>
|
|
</button>
|
|
${this.showSendToMenu ? this.renderSendToMenu() : ''}
|
|
</div>
|
|
|
|
<button
|
|
class="action-button secondary"
|
|
?disabled=${!this.currentDocument}
|
|
@click=${this.handleSaveLocal}
|
|
>
|
|
<dees-icon .icon=${'lucide:download'} .iconSize=${18}></dees-icon>
|
|
Save
|
|
</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private renderSendToMenu(): TemplateResult {
|
|
return html`
|
|
<div class="dropdown-menu" @mouseleave=${() => this.showSendToMenu = false}>
|
|
${this.providers.length > 0
|
|
? this.providers.map(provider => html`
|
|
<div class="dropdown-item" @click=${() => this.handleSendToProvider(provider.id)}>
|
|
<dees-icon .icon=${provider.icon || 'lucide:cloud'} .iconSize=${18}></dees-icon>
|
|
${provider.name}
|
|
</div>
|
|
`)
|
|
: html`
|
|
<div class="dropdown-item" style="opacity: 0.5; cursor: default;">
|
|
<dees-icon .icon=${'lucide:info'} .iconSize=${18}></dees-icon>
|
|
No providers configured
|
|
</div>
|
|
`
|
|
}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private renderHistoryPanel(): TemplateResult {
|
|
return html`
|
|
<div class="panel-header">
|
|
<div class="panel-header-left">
|
|
<h2 class="panel-title">Recent Scans</h2>
|
|
<p class="panel-description">View and manage your scanned documents</p>
|
|
</div>
|
|
</div>
|
|
|
|
${this.scanHistory.length > 0
|
|
? html`
|
|
<div class="history-grid">
|
|
${this.scanHistory.map(doc => html`
|
|
<div class="history-item" @click=${() => this.handleHistoryItemClick(doc)}>
|
|
<div class="history-thumbnail">
|
|
${doc.thumbnail
|
|
? html`<img src=${doc.thumbnail} alt=${doc.name || 'Scan'} />`
|
|
: html`<dees-icon .icon=${'lucide:file'} .iconSize=${32}></dees-icon>`
|
|
}
|
|
</div>
|
|
<div class="history-info">
|
|
<div class="history-name">${doc.name || `Scan ${doc.id.slice(0, 8)}`}</div>
|
|
<div class="history-date">${this.formatDate(doc.timestamp)}</div>
|
|
</div>
|
|
</div>
|
|
`)}
|
|
</div>
|
|
`
|
|
: html`
|
|
<div class="empty-state">
|
|
<dees-icon .icon=${'lucide:inbox'} .iconSize=${48}></dees-icon>
|
|
<p>No scans yet. Start scanning to see your documents here.</p>
|
|
</div>
|
|
`
|
|
}
|
|
`;
|
|
}
|
|
|
|
private renderSettingsPanel(): TemplateResult {
|
|
return html`
|
|
<div class="panel-header">
|
|
<div class="panel-header-left">
|
|
<h2 class="panel-title">Scan Settings</h2>
|
|
<p class="panel-description">Configure default scan options</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="settings-section">
|
|
<div class="settings-section-title">Output Settings</div>
|
|
|
|
<div class="setting-row">
|
|
<span class="setting-label">Format</span>
|
|
<div class="setting-control">
|
|
<select
|
|
.value=${this.settings.format}
|
|
@change=${(e: Event) => this.updateSetting('format', (e.target as HTMLSelectElement).value as TScanFormat)}
|
|
>
|
|
${this.availableFormats.map(fmt => html`
|
|
<option value=${fmt} ?selected=${fmt === this.settings.format}>
|
|
${fmt.toUpperCase()}
|
|
</option>
|
|
`)}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="setting-row">
|
|
<span class="setting-label">Resolution (DPI)</span>
|
|
<div class="setting-control">
|
|
<select
|
|
.value=${String(this.settings.resolution)}
|
|
@change=${(e: Event) => this.updateSetting('resolution', parseInt((e.target as HTMLSelectElement).value))}
|
|
>
|
|
${this.availableResolutions.map(res => html`
|
|
<option value=${res} ?selected=${res === this.settings.resolution}>
|
|
${res} DPI
|
|
</option>
|
|
`)}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="setting-row">
|
|
<span class="setting-label">Color Mode</span>
|
|
<div class="setting-control">
|
|
<select
|
|
.value=${this.settings.colorMode}
|
|
@change=${(e: Event) => this.updateSetting('colorMode', (e.target as HTMLSelectElement).value as TScanColorMode)}
|
|
>
|
|
${this.availableColorModes.map(mode => html`
|
|
<option value=${mode} ?selected=${mode === this.settings.colorMode}>
|
|
${this.getColorModeLabel(mode)}
|
|
</option>
|
|
`)}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="setting-row">
|
|
<span class="setting-label">Source</span>
|
|
<div class="setting-control">
|
|
<select
|
|
.value=${this.settings.source}
|
|
@change=${(e: Event) => this.updateSetting('source', (e.target as HTMLSelectElement).value as TScanSource)}
|
|
>
|
|
${this.availableSources.map(src => html`
|
|
<option value=${src} ?selected=${src === this.settings.source}>
|
|
${this.getSourceLabel(src)}
|
|
</option>
|
|
`)}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private handleScannerChange(e: Event): void {
|
|
const select = e.target as HTMLSelectElement;
|
|
this.selectedScannerId = select.value || null;
|
|
this.dispatchEvent(new CustomEvent('scanner-select', {
|
|
detail: { scannerId: this.selectedScannerId },
|
|
bubbles: true,
|
|
composed: true,
|
|
}));
|
|
}
|
|
|
|
private async handleScan(): Promise<void> {
|
|
if (!this.selectedScannerId || this.isScanning) return;
|
|
|
|
this.isScanning = true;
|
|
this.dispatchEvent(new CustomEvent('scan-request', {
|
|
detail: {
|
|
scannerId: this.selectedScannerId,
|
|
settings: this.settings,
|
|
},
|
|
bubbles: true,
|
|
composed: true,
|
|
}));
|
|
}
|
|
|
|
public setScanResult(result: { data: string; format: TScanFormat; thumbnail?: string }): void {
|
|
const doc: IScannedDocument = {
|
|
id: crypto.randomUUID(),
|
|
timestamp: new Date(),
|
|
format: result.format,
|
|
data: result.data,
|
|
thumbnail: result.thumbnail,
|
|
size: result.data.length,
|
|
};
|
|
|
|
this.currentDocument = doc;
|
|
this.currentPreview = result.thumbnail || `data:image/${result.format};base64,${result.data}`;
|
|
this.scanHistory = [doc, ...this.scanHistory.slice(0, 19)]; // Keep last 20
|
|
this.isScanning = false;
|
|
}
|
|
|
|
public setScanError(error: string): void {
|
|
this.isScanning = false;
|
|
console.error('Scan error:', error);
|
|
// Could dispatch error event or show toast
|
|
}
|
|
|
|
private handleSaveLocal(): void {
|
|
if (!this.currentDocument) return;
|
|
|
|
this.dispatchEvent(new CustomEvent('save-local', {
|
|
detail: { document: this.currentDocument },
|
|
bubbles: true,
|
|
composed: true,
|
|
}));
|
|
}
|
|
|
|
private handleSendToProvider(providerId: string): void {
|
|
if (!this.currentDocument) return;
|
|
|
|
this.showSendToMenu = false;
|
|
this.dispatchEvent(new CustomEvent('send-to-provider', {
|
|
detail: {
|
|
providerId,
|
|
document: this.currentDocument,
|
|
},
|
|
bubbles: true,
|
|
composed: true,
|
|
}));
|
|
}
|
|
|
|
private handleHistoryItemClick(doc: IScannedDocument): void {
|
|
this.currentDocument = doc;
|
|
this.currentPreview = doc.thumbnail || `data:image/${doc.format};base64,${doc.data}`;
|
|
this.activePanel = 'scan';
|
|
}
|
|
|
|
private updateSetting<K extends keyof IScanSettings>(key: K, value: IScanSettings[K]): void {
|
|
this.settings = { ...this.settings, [key]: value };
|
|
this.dispatchEvent(new CustomEvent('settings-change', {
|
|
detail: { settings: this.settings },
|
|
bubbles: true,
|
|
composed: true,
|
|
}));
|
|
}
|
|
|
|
private getColorModeLabel(mode: TScanColorMode): string {
|
|
const labels: Record<TScanColorMode, string> = {
|
|
color: 'Color',
|
|
grayscale: 'Grayscale',
|
|
blackwhite: 'Black & White',
|
|
};
|
|
return labels[mode];
|
|
}
|
|
|
|
private getSourceLabel(source: TScanSource): string {
|
|
const labels: Record<TScanSource, string> = {
|
|
flatbed: 'Flatbed',
|
|
adf: 'Document Feeder',
|
|
'adf-duplex': 'Document Feeder (Duplex)',
|
|
};
|
|
return labels[source];
|
|
}
|
|
|
|
private formatDate(date: Date): string {
|
|
return new Intl.DateTimeFormat('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
}).format(date);
|
|
}
|
|
}
|