@@ -6,6 +6,7 @@ import { VirtualGrid, type VirtualMetrics } from '../virtual';
66import { icon } from './icons' ;
77import { openMenu } from './menu' ;
88import { editBookmark , pickFolder } from './dialogs' ;
9+ import { confirmDialog } from './modal' ;
910import { toast , notice } from './toast' ;
1011
1112const METRICS : Record < ViewMode , VirtualMetrics > = {
@@ -114,10 +115,14 @@ export class ListView {
114115 host . classList . add ( `view-${ store . settings . view } ` ) ;
115116 }
116117
118+ private get isTrash ( ) : boolean {
119+ return store . scope . kind === 'trash' ;
120+ }
121+
117122 private items ( ) : Bookmark [ ] {
118123 const out : Bookmark [ ] = [ ] ;
119124 for ( const id of store . visible ) {
120- const b = store . bookmarks . get ( id ) ;
125+ const b = store . getBookmark ( id ) ;
121126 if ( b ) out . push ( b ) ;
122127 }
123128 return out ;
@@ -133,6 +138,15 @@ export class ListView {
133138 }
134139
135140 private paintEmpty ( ) : void {
141+ if ( this . isTrash ) {
142+ const searching = store . query . trim ( ) . length > 0 ;
143+ this . emptyBox . replaceChildren (
144+ el ( 'div' , { class : 'empty__art' , html : icon ( searching ? 'search' : 'trash' ) } ) ,
145+ el ( 'div' , { class : 'empty__title' , text : searching ? '回收站里没有匹配项' : '回收站是空的' } ) ,
146+ el ( 'p' , { class : 'empty__desc' , text : searching ? '换个关键词试试。' : '删除的书签会先进入这里,可随时恢复或彻底清除。' } ) ,
147+ ) ;
148+ return ;
149+ }
136150 const hasData = store . bookmarks . size > 0 ;
137151 const searching = store . query . trim ( ) . length > 0 ;
138152 const title = searching ? '没有匹配的书签' : hasData ? '这个分类还是空的' : '开始导入你的书签' ;
@@ -232,13 +246,26 @@ export class ListView {
232246 }
233247
234248 const folder = node . querySelector < HTMLElement > ( '.item__folder' ) ! ;
235- const path = b . folderId ? store . folders . get ( b . folderId ) ?. name : null ;
249+ const path = store . folderNameOf ( b . folderId ) ;
236250 folder . textContent = path ?? '未分类' ;
237251 folder . classList . toggle ( 'chip--accent' , ! path ) ;
238252
239- const star = node . querySelector < HTMLElement > ( '[data-act="star"]' ) ! ;
240- star . classList . toggle ( 'is-on' , b . favorite ) ;
241- star . innerHTML = icon ( b . favorite ? 'starOn' : 'star' ) ;
253+ const actions = node . querySelector < HTMLElement > ( '.item__actions' ) ! ;
254+ const trash = this . isTrash ;
255+ if ( actions . dataset . mode !== ( trash ? 'trash' : 'normal' ) ) {
256+ actions . dataset . mode = trash ? 'trash' : 'normal' ;
257+ actions . innerHTML = trash
258+ ? `<button class="item__act" data-act="restore" type="button" tabindex="-1" title="恢复">${ icon ( 'undo' ) } </button>
259+ <button class="item__act item__act--danger" data-act="purge" type="button" tabindex="-1" title="彻底删除">${ icon ( 'trash' ) } </button>`
260+ : `<button class="item__act" data-act="star" type="button" tabindex="-1" title="星标">${ icon ( 'star' ) } </button>
261+ <button class="item__act" data-act="edit" type="button" tabindex="-1" title="编辑">${ icon ( 'pencil' ) } </button>
262+ <button class="item__act item__act--danger" data-act="del" type="button" tabindex="-1" title="删除">${ icon ( 'trash' ) } </button>` ;
263+ }
264+ if ( ! trash ) {
265+ const star = actions . querySelector < HTMLElement > ( '[data-act="star"]' ) ! ;
266+ star . classList . toggle ( 'is-on' , b . favorite ) ;
267+ star . innerHTML = icon ( b . favorite ? 'starOn' : 'star' ) ;
268+ }
242269
243270 const cb = node . querySelector < HTMLInputElement > ( 'input[type=checkbox]' ) ! ;
244271 cb . checked = store . selection . has ( b . id ) ;
@@ -296,6 +323,11 @@ export class ListView {
296323 } ) ;
297324
298325 node . addEventListener ( 'dragstart' , ( e ) => {
326+ // 回收站里的书签不参与归类拖拽
327+ if ( this . isTrash ) {
328+ e . preventDefault ( ) ;
329+ return ;
330+ }
299331 const id = this . idOf ( node ) ;
300332 const ids = store . selection . has ( id ) ? [ ...store . selection ] : [ id ] ;
301333 e . dataTransfer ?. setData ( 'application/x-cozytag' , JSON . stringify ( ids ) ) ;
@@ -318,14 +350,14 @@ export class ListView {
318350 }
319351
320352 private open ( id : string ) : void {
321- const b = store . bookmarks . get ( id ) ;
353+ const b = store . getBookmark ( id ) ;
322354 if ( ! b ) return ;
323355 if ( store . settings . openInNewTab ) window . open ( b . url , '_blank' , 'noopener,noreferrer' ) ;
324356 else location . href = b . url ;
325357 }
326358
327359 private async runAction ( act : string , id : string ) : Promise < void > {
328- const b = store . bookmarks . get ( id ) ;
360+ const b = store . getBookmark ( id ) ;
329361 if ( ! b ) return ;
330362 switch ( act ) {
331363 case 'star' :
@@ -339,14 +371,66 @@ export class ListView {
339371 notice ( '已移入回收站' , { kind : 'ok' } ) ;
340372 toast ( '已删除 1 条书签' , { action : { label : '撤销' , onClick : ( ) => void store . undo ( ) } } ) ;
341373 break ;
374+ case 'restore' :
375+ await store . restoreBookmarks ( [ id ] ) ;
376+ notice ( '已恢复' , { kind : 'ok' } ) ;
377+ toast ( '已恢复 1 条书签' ) ;
378+ break ;
379+ case 'purge' : {
380+ const ok = await confirmDialog ( {
381+ title : '彻底删除' ,
382+ message : '该书签将被永久删除,无法恢复。' ,
383+ confirmText : '彻底删除' ,
384+ danger : true ,
385+ } ) ;
386+ if ( ok ) {
387+ await store . purgeBookmarks ( [ id ] ) ;
388+ notice ( '已彻底删除' , { kind : 'warn' } ) ;
389+ }
390+ break ;
391+ }
342392 }
343393 }
344394
345395 private itemMenu ( id : string , x : number , y : number ) : void {
346- const b = store . bookmarks . get ( id ) ;
396+ const b = store . getBookmark ( id ) ;
347397 if ( ! b ) return ;
348398 const ids = store . selection . size && store . selection . has ( id ) ? [ ...store . selection ] : [ id ] ;
349399 const many = ids . length > 1 ;
400+ if ( this . isTrash ) {
401+ openMenu ( x , y , [
402+ { label : many ? `打开 ${ ids . length } 个标签页` : '打开' , icon : 'external' , onClick : ( ) => ids . slice ( 0 , 20 ) . forEach ( ( i ) => this . open ( i ) ) } ,
403+ { label : '复制链接' , icon : 'copy' , onClick : ( ) => void navigator . clipboard . writeText ( ids . map ( ( i ) => store . getBookmark ( i ) ?. url ?? '' ) . join ( '\n' ) ) . then ( ( ) => toast ( '链接已复制' ) ) } ,
404+ 'sep' ,
405+ {
406+ label : many ? `恢复 ${ ids . length } 条` : '恢复' ,
407+ icon : 'undo' ,
408+ onClick : async ( ) => {
409+ const n = await store . restoreBookmarks ( ids ) ;
410+ notice ( `已恢复 ${ n } 条书签` , { kind : 'ok' } ) ;
411+ toast ( `已恢复 ${ n } 条书签` ) ;
412+ } ,
413+ } ,
414+ {
415+ label : many ? `彻底删除 ${ ids . length } 条` : '彻底删除' ,
416+ icon : 'trash' ,
417+ danger : true ,
418+ onClick : async ( ) => {
419+ const ok = await confirmDialog ( {
420+ title : many ? `彻底删除 ${ ids . length } 条书签` : '彻底删除' ,
421+ message : '选中的书签将被永久删除,无法恢复。' ,
422+ confirmText : '彻底删除' ,
423+ danger : true ,
424+ } ) ;
425+ if ( ok ) {
426+ const n = await store . purgeBookmarks ( ids ) ;
427+ notice ( `已彻底删除 ${ n } 条书签` , { kind : 'warn' } ) ;
428+ }
429+ } ,
430+ } ,
431+ ] ) ;
432+ return ;
433+ }
350434 openMenu ( x , y , [
351435 { label : many ? `打开 ${ ids . length } 个标签页` : '打开' , icon : 'external' , onClick : ( ) => ids . slice ( 0 , 20 ) . forEach ( ( i ) => this . open ( i ) ) } ,
352436 { label : '复制链接' , icon : 'copy' , onClick : ( ) => void navigator . clipboard . writeText ( ids . map ( ( i ) => store . bookmarks . get ( i ) ?. url ?? '' ) . join ( '\n' ) ) . then ( ( ) => toast ( '链接已复制' ) ) } ,
0 commit comments