@ -1,5 +1,7 @@
<!-- lib/pages/print/PrintProgress.svelte -->
< script >
let { store , onBack = () => {} } = $props ();
const currentStatus = $derived . by (() => {
if ( ! store . job ) return 'idle' ;
const f = store . job . files . find ( f => f . status === 'printing' );
@ -8,7 +10,6 @@ const currentStatus = $derived.by(() => {
return q ? 'queued' : store . job . phase ;
});
let { store , onBack = () => {} } = $props ();
const OFFICE_EXT = [ 'DOC' , 'DOCX' , 'ODT' , 'XLS' , 'XLSX' , 'ODS' , 'PPT' , 'PPTX' , 'ODP' ];
const STATUS_LABEL = {
@ -17,7 +18,7 @@ const STATUS_LABEL = {
sent : 'Отправлен' ,
awaiting_clear_output : 'Уберите распечатанные листы' ,
awaiting_flip : 'Переложите бумагу' ,
awaiting_pickup : 'Заберите документ ',
awaiting_pickup : 'Печать завершается... ' ,
done : 'Готов' ,
error : 'Ошибка' ,
cancelled : 'Отменено'
@ -34,8 +35,9 @@ $effect(() => {
return () => clearInterval ( int );
});
// Проверка, можно ли отменить (есть ли файлы в очереди или печатающиеся)
const canCancel = $derived ( store . job ? . files . some ( f => f . status === 'queued' || f . status === 'printing' ));
const canCancel = $derived (
store . job ? . files . some ( f => f . status === 'queued' || f . status === 'printing' )
);
const HOLD_MS = 1500 ;
let hold = $state ( 0 );
@ -49,12 +51,7 @@ function startHold(e) {
t0 = performance . now ();
const step = ( now ) => {
hold = Math . min ( 1 , ( now - t0 ) / HOLD_MS );
if ( hold >= 1 ) {
raf = 0 ;
hold = 0 ;
store . cancelJob ();
return ;
}
if ( hold >= 1 ) { raf = 0 ; hold = 0 ; store . cancelJob (); return ; }
raf = requestAnimationFrame ( step );
};
raf = requestAnimationFrame ( step );
@ -77,32 +74,39 @@ function finish() {
< button class = "back-btn" disabled = { store . jobActive } onclick= { finish } > ←</ button >
< h1 > Печать</ h1 >
</ header >
< main class = "list" >
{ #if store . job }
{ #each store . job . files as f , i ( i + '-' + f . name )}
{ @const b = badge ( f . name )}
{ @const isLastDon e = f . status === 'done' && ! store . job . files . slice ( i + 1 ). some ( f => f . status === 'printing' || f . status === 'queued' )}
{ @const hasNextQueued = store . job . files . slice ( i + 1 ). some ( f => f . status === 'queued' )}
< section class = "file-card" class:current = { f . status === 'awaiting_flip' || f . status === 'awaiting_clear_output' || f . status === 'printing' || isLastDone } >
{ @const isActiv e = f . status === 'awaiting_flip'
|| f . status === 'awaiting_clear_output'
|| f . status === 'awaiting_pickup'
|| f . status === 'printing' }
< section class = "file-card" class:current = { isActive } >
< div class = "file-row" >
{ #if b } < span class = "type-badge" > { b } </ span > { /if }
< span class = "file-name" title = { f . name } > { f . name } </span >
< span class = "file-status st- { f . status } " > { STATUS_LABEL [ f . status ] ?? f . status } </ span >
< span class = "file-status st- { f . status } " >
{ STATUS_LABEL [ f . status ] ?? f . status }
</ span >
</ div >
{ #if f . status === 'awaiting_clear_output ' }
{ #if f . status === 'awaiting_pickup ' }
< ol class = "pickup-hint" >
< li > Дождитесь завершения печати</ li >
< li > Нажмите кнопку ниже для подтверждения</ li >
</ ol >
{ :else if f . status === 'awaiting_clear_output' }
< ol class = "pickup-hint clear-hint" >
< li > Уберите распечатанные листы из выходного лотка</ li >
< li > Нажмите кнопку ниже для запуска двусторонней печати </ li >
< li > Нажмите кнопку ниже для продолжения </ li >
</ ol >
{ :else if f . status === 'awaiting_flip' }
< ol class = "pickup-hint flip-hint" >
< li > Н е переворачивая, положите остальные листы в слот ручной подачи</ li >
</ o l>
{ :else if isLastDone && hasNextQueued && ! store . autoContinue }
< ol class = "pickup-hint" >
< li > Возьмите готовый документ</ li >
< li > Нажмите далее</ li >
< li > Н е переворачивая, положите листы в слот ручной подачи</ li >
< li > Нажмите кнопку ниже для печати обратной стороны</ li >
</ ol >
{ /if }
@ -113,6 +117,7 @@ function finish() {
{ /each }
{ /if }
</ main >
< footer class = "actions" >
< label class = "auto-row" >
< input
@ -128,16 +133,18 @@ function finish() {
{ /if }
{ #if store . jobActive }
{ #if store . job ? . phase === 'awaiting_clear_output ' }
{ #if store . job ? . phase === 'awaiting_pickup ' }
< button class = "next-btn" onclick = { store . advanceJob } >
Печать закончена — далее
</ button >
{ :else if store . job ? . phase === 'awaiting_clear_output' }
< button class = "next-btn next-btn-clear" onclick = { store . advanceJob } >
Убрал листы — печат ать
Убрал листы — продолж ать
</ button >
{ :else if store . job ? . phase === 'awaiting_flip' }
< button class = "next-btn next-btn-flip" onclick = { store . advanceJob } >
Переложил — печатать обратную сторону
</ button >
{ :else if store . job ? . phase === 'awaiting_pickup' }
< button class = "next-btn" onclick = { store . advanceJob } > Забрал — далее </ button >
{ : else }
< button class = "next-btn next-btn-disabled" disabled >
{ currentStatus === 'printing' ? 'Отправка...' : 'Ожидание...' }
@ -156,6 +163,7 @@ function finish() {
< span class = "cancel-fill" style:width = " { hold * 100 } %" ></ span >
< span class = "cancel-label" > { canCancel ? 'Отмена' : 'Отмена недоступна' } </ span >
</ button >
{ #if canCancel }
< p class = "hold-hint" > Удерживайте для отмены печати</ p >
{ /if }
@ -168,17 +176,17 @@ function finish() {
< style >
. st-awaiting_clear_output { color : #f59e0b ; }
. st-awaiting_flip { color : #f59e0b ; }
. st-sent { color : #8b93a1 ; }
. st-awaiting_pickup { color : #a5b4fc ; }
. st-sent { color : #8b93a1 ; }
. st-done { color : #22c55e ; }
. st-queued { color : #8b93a1 ; }
. st-printing { color : #a5b4fc ; }
. st-error { color : #ef4444 ; }
. st-cancelled { color : #6b7280 ; }
. clear-hint { color : #f59e0b ; }
. flip-hint { color : #f59e0b ; }
. next-btn-clear {
background : linear-gradient ( 135 deg , #f59e0b 0 % , #d97706 100 % );
box-shadow : 0 8 px 24 px -6 px rgba ( 245 , 158 , 11 , 0.4 );
}
. auto-warning {
margin : -4 px 0 8 px ;
text-align : center ;
@ -187,48 +195,111 @@ function finish() {
font-weight : 600 ;
}
. page-container { width : 100 % ; min-height : 100 dvh ; display : flex ; flex-direction : column ; background : radial-gradient ( 120 % 60 % at 50 % -10 % , rgba ( 99 , 102 , 241 , 0.18 ) 0 % , transparent 60 % ); font-family : system-ui , - apple-system , sans-serif ; color : #f5f7fa ; }
. page-container {
width : 100 % ; min-height : 100 dvh ;
display : flex ; flex-direction : column ;
background : radial-gradient ( 120 % 60 % at 50 % -10 % , rgba ( 99 , 102 , 241 , 0.18 ) 0 % , transparent 60 % );
font-family : system-ui , - apple-system , sans-serif ;
color : #f5f7fa ;
}
. header { padding : 24 px 20 px 8 px ; display : flex ; align-items : center ; position : relative ; }
. back-btn { background : none ; border : none ; color : #6366f1 ; font-size : 15 px ; font-weight : 600 ; cursor : pointer ; padding : 8 px 4 px ; margin-right : auto ; min-height : 44 px ; }
. back-btn {
background : none ; border : none ; color : #6366f1 ;
font-size : 15 px ; font-weight : 600 ; cursor : pointer ;
padding : 8 px 4 px ; margin-right : auto ; min-height : 44 px ;
}
. back-btn : disabled { opacity : 0.3 ; cursor : not-allowed ; }
. header h1 { margin : 0 ; font-size : 28 px ; font-weight : 700 ; position : absolute ; left : 50 % ; transform : translateX ( -50 % ); white-space : nowrap ; }
. header h1 {
margin : 0 ; font-size : 28 px ; font-weight : 700 ;
position : absolute ; left : 50 % ; transform : translateX ( -50 % );
white-space : nowrap ;
}
. list { flex : 1 ; padding : 24 px 20 px 16 px ; display : flex ; flex-direction : column ; gap : 14 px ; overflow-y : auto ; }
. file-card { border-radius : 16 px ; background : rgba ( 255 , 255 , 255 , 0.04 ); border : 1 px solid rgba ( 255 , 255 , 255 , 0.08 ); padding : 14 px ; display : flex ; flex-direction : column ; gap : 8 px ; transition : border-color 0.2 s ease , background 0.2 s ease ; }
. file-card {
border-radius : 16 px ; background : rgba ( 255 , 255 , 255 , 0.04 );
border : 1 px solid rgba ( 255 , 255 , 255 , 0.08 );
padding : 14 px ; display : flex ; flex-direction : column ; gap : 8 px ;
transition : border-color 0.2 s ease , background 0.2 s ease ;
}
. file-card . current { border-color : rgba ( 99 , 102 , 241 , 0.45 ); background : rgba ( 99 , 102 , 241 , 0.07 ); }
. file-row { display : flex ; align-items : center ; gap : 12 px ; min-width : 0 ; }
. file-name { flex : 1 ; min-width : 0 ; font-size : 15 px ; font-weight : 500 ; overflow : hidden ; text-overflow : ellipsis ; white-space : nowrap ; }
. file-name {
flex : 1 ; min-width : 0 ; font-size : 15 px ; font-weight : 500 ;
overflow : hidden ; text-overflow : ellipsis ; white-space : nowrap ;
}
. file-status { flex-shrink : 0 ; font-size : 14 px ; font-weight : 700 ; }
. st-done { color : #22c55e ; }
. st-queued { color : #8b93a1 ; }
. st-printing { color : #a5b4fc ; }
. st-error { color : #ef4444 ; }
. st-cancelled { color : #6b728 0; }
. type-badge { flex-shrink : 0 ; padding : 3 px 7 px ; border-radius : 6 px ; background : rgba ( 99 , 102 , 241 , 0.2 ); border : 1 px solid rgba ( 99 , 102 , 241 , 0.35 ); color : #a5b4fc ; font-size : 10 px ; font-weight : 700 ; letter-spacing : 0.04 em ; line-height : 1.2 ; user-select : none ; }
. pickup-hint { margin : 0 ; padding-left : 22 px ; color : #eab308 ; font-size : 14 px ; line-height : 1.6 ; font-weight : 600 ; }
. type-badge {
flex-shrink : 0 ; padding : 3 px 7 px ; border-radius : 6 px ;
background : rgba ( 99 , 102 , 241 , 0.2 ); border : 1 px solid rgba ( 99 , 102 , 241 , 0.35 );
color : #a5b4fc ; font-size : 10 px ; font-weight : 70 0;
letter-spacing : 0.04 em ; line-height : 1.2 ; user-select : none ;
}
. pickup-hint {
margin : 0 ; padding-left : 22 px ;
color : #eab308 ; font-size : 14 px ; line-height : 1.6 ; font-weight : 600 ;
}
. file-error { margin : 0 ; color : #fca5a5 ; font-size : 12 px ; line-height : 1.4 ; }
. actions { padding : 16 px 20 px 32 px ; display : flex ; flex-direction : column ; gap : 12 px ; }
. auto-row { display : flex ; align-items : center ; justify-content : center ; gap : 10 px ; color : #a9b0c0 ; font-size : 14 px ; font-weight : 600 ; user-select : none ; cursor : pointer ; }
. auto-row {
display : flex ; align-items : center ; justify-content : center ; gap : 10 px ;
color : #a9b0c0 ; font-size : 14 px ; font-weight : 600 ;
user-select : none ; cursor : pointer ;
}
. auto-row input { width : 18 px ; height : 18 px ; accent-color : #6366f1 ; cursor : pointer ; }
. next-btn { width : 100 % ; padding : 16 px 24 px ; border-radius : 16 px ; font-size : 16 px ; font-weight : 700 ; cursor : pointer ; border : none ; color : #fff ; background : linear-gradient ( 135 deg , #5b8cff 0 % , #38c6ff 100 % ); box-shadow : 0 8 px 24 px -6 px rgba ( 72 , 150 , 255 , 0.4 ); }
. next-btn {
width : 100 % ; padding : 16 px 24 px ; border-radius : 16 px ;
font-size : 16 px ; font-weight : 700 ; cursor : pointer ; border : none ; color : #fff ;
background : linear-gradient ( 135 deg , #5b8cff 0 % , #38c6ff 100 % );
box-shadow : 0 8 px 24 px -6 px rgba ( 72 , 150 , 255 , 0.4 );
}
. next-btn-clear {
background : linear-gradient ( 135 deg , #f59e0b 0 % , #d97706 100 % );
box-shadow : 0 8 px 24 px -6 px rgba ( 245 , 158 , 11 , 0.4 );
}
. next-btn-flip {
background : linear-gradient ( 135 deg , #f59e0b 0 % , #d97706 100 % );
box-shadow : 0 8 px 24 px -6 px rgba ( 245 , 158 , 11 , 0.4 );
}
. next-btn-disabled {
opacity : 0.5 ;
cursor : wait ;
pointer-events : none ;
opacity : 0.5 ; cursor : wait ; pointer-events : none ;
background : linear-gradient ( 135 deg , #475569 0 % , #64748b 100 % );
box-shadow : none ;
}
. cancel-btn { position : relative ; overflow : hidden ; width : 100 % ; padding : 16 px 24 px ; border-radius : 16 px ; border : 1 px solid rgba ( 239 , 68 , 68 , 0.35 ); background : rgba ( 239 , 68 , 68 , 0.08 ); color : #fca5a5 ; font-size : 16 px ; font-weight : 700 ; cursor : pointer ; touch-action : none ; user-select : none ; -webkit- user-select : none ; -webkit- touch-callout : none ; -webkit- tap-highlight-color : transparent ; }
. cancel-btn : disabled {
opacity : 0.4 ;
cursor : not-allowed ;
pointer-events : none ;
. cancel-btn {
position : relative ; overflow : hidden ; width : 100 % ; padding : 16 px 24 px ;
border-radius : 16 px ; border : 1 px solid rgba ( 239 , 68 , 68 , 0.35 ) ;
background : rgba ( 239 , 68 , 68 , 0.08 ); color : #fca5a5 ;
font-size : 16 px ; font-weight : 700 ; cursor : pointer ;
touch-action : none ; user-select : none ;
-webkit- user-select : none ; -webkit- touch-callout : none ;
-webkit- tap-highlight-color : transparent ;
}
. cancel-btn : disabled { opacity : 0.4 ; cursor : not-allowed ; pointer-events : none ; }
. cancel-fill {
position : absolute ; left : 0 ; top : 0 ; bottom : 0 ; width : 0 % ;
background : rgba ( 239 , 68 , 68 , 0.35 ); pointer-events : none ;
}
. cancel-fill { position : absolute ; left : 0 ; top : 0 ; bottom : 0 ; width : 0 % ; background : rgba ( 239 , 68 , 68 , 0.35 ); pointer-events : none ; }
. cancel-label { position : relative ; }
. hold-hint { margin : -4 px 0 0 ; text-align : center ; color : rgba ( 139 , 147 , 161 , 0.6 ); font-size : 11 px ; }
. home-btn { width : 100 % ; padding : 16 px 24 px ; border-radius : 16 px ; font-size : 16 px ; font-weight : 700 ; cursor : pointer ; border : none ; color : #fff ; background : linear-gradient ( 135 deg , #22c55e 0 % , #16a34a 100 % ); box-shadow : 0 8 px 24 px -6 px rgba ( 34 , 197 , 94 , 0.4 ); }
. home-btn {
width : 100 % ; padding : 16 px 24 px ; border-radius : 16 px ;
font-size : 16 px ; font-weight : 700 ; cursor : pointer ; border : none ; color : #fff ;
background : linear-gradient ( 135 deg , #22c55e 0 % , #16a34a 100 % );
box-shadow : 0 8 px 24 px -6 px rgba ( 34 , 197 , 94 , 0.4 );
}
</ style >