@@ -3,6 +3,9 @@ import { createPortal } from "react-dom";
33import { motion , AnimatePresence } from "framer-motion" ;
44import { Cross2Icon , ChevronDownIcon , DownloadIcon } from "@radix-ui/react-icons" ;
55import { domToCanvas } from "modern-screenshot" ;
6+ import { save } from "@tauri-apps/plugin-dialog" ;
7+ import { invoke } from "@tauri-apps/api/core" ;
8+ import { revealItemInDir } from "@tauri-apps/plugin-opener" ;
69import type { AnnualReport2025 as AnnualReportData } from "../../types" ;
710import { useInvokeQuery } from "../../hooks" ;
811
@@ -482,48 +485,89 @@ function ShareableCard({ report }: { report: AnnualReportData }) {
482485 boxShadow : "0 8px 32px rgba(0,0,0,0.2)" ,
483486 } }
484487 >
485- { /* 2x2 Core Stats Grid - Unified warm colors */ }
488+ { /* Stats Period Badge */ }
489+ < div
490+ style = { {
491+ textAlign : "center" ,
492+ marginBottom : 12 ,
493+ paddingBottom : 12 ,
494+ borderBottom : "1px solid rgba(216,90,42,0.1)" ,
495+ } }
496+ >
497+ < span
498+ style = { {
499+ fontSize : 10 ,
500+ color : "#AA7A6A" ,
501+ background : "rgba(216,90,42,0.08)" ,
502+ padding : "4px 12px" ,
503+ borderRadius : 10 ,
504+ fontWeight : 500 ,
505+ } }
506+ >
507+ 统计周期:{ report . first_chat_date || "2025.01" } - { report . last_chat_date || "2025.12" }
508+ </ span >
509+ </ div >
510+
511+ { /* 3-column Core Stats Grid */ }
486512 < div
487513 style = { {
488514 display : "grid" ,
489- gridTemplateColumns : "1fr 1fr" ,
490- gap : 12 ,
515+ gridTemplateColumns : "1fr 1fr 1fr " ,
516+ gap : 8 ,
491517 } }
492518 >
493519 { /* Conversations */ }
494- < div style = { { textAlign : "center" , padding : 12 } } >
495- < div style = { { fontSize : 11 , color : "#996B5B" , marginBottom : 4 , fontWeight : 600 } } > 对话总数 </ div >
496- < div style = { { fontSize : 38 , fontWeight : 800 , color : "#D85A2A" , lineHeight : 1 } } >
520+ < div style = { { textAlign : "center" , padding : 10 } } >
521+ < div style = { { fontSize : 10 , color : "#996B5B" , marginBottom : 4 , fontWeight : 600 } } > 对话 </ div >
522+ < div style = { { fontSize : 28 , fontWeight : 800 , color : "#D85A2A" , lineHeight : 1 } } >
497523 { report . total_sessions . toLocaleString ( ) }
498524 </ div >
499- < div style = { { fontSize : 11 , color : "#AA7A6A" , marginTop : 2 } } > 次</ div >
525+ < div style = { { fontSize : 10 , color : "#AA7A6A" , marginTop : 2 } } > 次</ div >
500526 </ div >
501527
502528 { /* Messages */ }
503- < div style = { { textAlign : "center" , padding : 12 } } >
504- < div style = { { fontSize : 11 , color : "#996B5B" , marginBottom : 4 , fontWeight : 600 } } > 消息数量 </ div >
505- < div style = { { fontSize : 38 , fontWeight : 800 , color : "#E8734A" , lineHeight : 1 } } >
529+ < div style = { { textAlign : "center" , padding : 10 } } >
530+ < div style = { { fontSize : 10 , color : "#996B5B" , marginBottom : 4 , fontWeight : 600 } } > 消息 </ div >
531+ < div style = { { fontSize : 28 , fontWeight : 800 , color : "#E8734A" , lineHeight : 1 } } >
506532 { report . total_messages . toLocaleString ( ) }
507533 </ div >
508- < div style = { { fontSize : 11 , color : "#AA7A6A" , marginTop : 2 } } > 条</ div >
534+ < div style = { { fontSize : 10 , color : "#AA7A6A" , marginTop : 2 } } > 条</ div >
535+ </ div >
536+
537+ { /* Commands */ }
538+ < div style = { { textAlign : "center" , padding : 10 } } >
539+ < div style = { { fontSize : 10 , color : "#996B5B" , marginBottom : 4 , fontWeight : 600 } } > 命令</ div >
540+ < div style = { { fontSize : 28 , fontWeight : 800 , color : "#D85A2A" , lineHeight : 1 } } >
541+ { report . total_commands . toLocaleString ( ) }
542+ </ div >
543+ < div style = { { fontSize : 10 , color : "#AA7A6A" , marginTop : 2 } } > 个</ div >
509544 </ div >
510545
511546 { /* Active Days */ }
512- < div style = { { textAlign : "center" , padding : 12 } } >
513- < div style = { { fontSize : 11 , color : "#996B5B" , marginBottom : 4 , fontWeight : 600 } } > 活跃天数 </ div >
514- < div style = { { fontSize : 38 , fontWeight : 800 , color : "#D85A2A " , lineHeight : 1 } } >
547+ < div style = { { textAlign : "center" , padding : 10 } } >
548+ < div style = { { fontSize : 10 , color : "#996B5B" , marginBottom : 4 , fontWeight : 600 } } > 活跃 </ div >
549+ < div style = { { fontSize : 28 , fontWeight : 800 , color : "#E8734A " , lineHeight : 1 } } >
515550 { report . active_days }
516551 </ div >
517- < div style = { { fontSize : 11 , color : "#AA7A6A" , marginTop : 2 } } > 天</ div >
552+ < div style = { { fontSize : 10 , color : "#AA7A6A" , marginTop : 2 } } > 天</ div >
518553 </ div >
519554
520555 { /* Projects */ }
521- < div style = { { textAlign : "center" , padding : 12 } } >
522- < div style = { { fontSize : 11 , color : "#996B5B" , marginBottom : 4 , fontWeight : 600 } } > 项目数量 </ div >
523- < div style = { { fontSize : 38 , fontWeight : 800 , color : "#E8734A " , lineHeight : 1 } } >
556+ < div style = { { textAlign : "center" , padding : 10 } } >
557+ < div style = { { fontSize : 10 , color : "#996B5B" , marginBottom : 4 , fontWeight : 600 } } > 项目 </ div >
558+ < div style = { { fontSize : 28 , fontWeight : 800 , color : "#D85A2A " , lineHeight : 1 } } >
524559 { report . total_projects }
525560 </ div >
526- < div style = { { fontSize : 11 , color : "#AA7A6A" , marginTop : 2 } } > 个</ div >
561+ < div style = { { fontSize : 10 , color : "#AA7A6A" , marginTop : 2 } } > 个</ div >
562+ </ div >
563+
564+ { /* Longest Streak */ }
565+ < div style = { { textAlign : "center" , padding : 10 } } >
566+ < div style = { { fontSize : 10 , color : "#996B5B" , marginBottom : 4 , fontWeight : 600 } } > 连续</ div >
567+ < div style = { { fontSize : 28 , fontWeight : 800 , color : "#E8734A" , lineHeight : 1 } } >
568+ { report . longest_streak }
569+ </ div >
570+ < div style = { { fontSize : 10 , color : "#AA7A6A" , marginTop : 2 } } > 天</ div >
527571 </ div >
528572 </ div >
529573
@@ -574,22 +618,17 @@ function ShareableCard({ report }: { report: AnnualReportData }) {
574618 marginBottom : 16 ,
575619 } }
576620 >
577- { /* Secondary Stats Row */ }
621+ { /* Secondary Stats Row - Peak Time Info */ }
578622 < div
579623 style = { {
580624 display : "grid" ,
581- gridTemplateColumns : "1fr 1fr 1fr " ,
582- gap : 8 ,
625+ gridTemplateColumns : "1fr 1fr" ,
626+ gap : 12 ,
583627 paddingBottom : 14 ,
584628 borderBottom : "1px solid rgba(255,255,255,0.1)" ,
585629 marginBottom : 14 ,
586630 } }
587631 >
588- < div style = { { textAlign : "center" } } >
589- < div style = { { fontSize : 10 , color : "rgba(255,255,255,0.6)" , marginBottom : 2 } } > 最长连续</ div >
590- < div style = { { fontSize : 24 , fontWeight : 800 , color : "#FFFFFF" , lineHeight : 1 } } > { report . longest_streak } </ div >
591- < div style = { { fontSize : 10 , color : "rgba(255,255,255,0.5)" } } > 天</ div >
592- </ div >
593632 < div style = { { textAlign : "center" } } >
594633 < div style = { { fontSize : 10 , color : "rgba(255,255,255,0.6)" , marginBottom : 2 } } > 活跃时段</ div >
595634 < div style = { { fontSize : 24 , fontWeight : 800 , color : "#FFFFFF" , lineHeight : 1 } } > { report . peak_hour } :00</ div >
@@ -639,18 +678,24 @@ function ShareableCard({ report }: { report: AnnualReportData }) {
639678 ) }
640679 </ div >
641680
642- { /* Footer - Compact */ }
643- < div style = { { textAlign : "center" } } >
644- < div style = { { display : "flex" , alignItems : "center" , justifyContent : "center" , gap : 6 , marginBottom : 4 } } >
681+ { /* Footer - with QR code */ }
682+ < div style = { { display : "flex" , alignItems : " center" , justifyContent : "space-between " } } >
683+ < div style = { { display : "flex" , alignItems : "center" , gap : 6 } } >
645684 < img
646685 src = "/logo.png"
647686 alt = "Lovcode"
648687 style = { { width : 20 , height : 20 , borderRadius : 4 } }
649688 />
650- < span style = { { fontSize : 13 , fontWeight : 700 , color : "#FFFFFF" } } > Lovcode</ span >
651- < span style = { { fontSize : 10 , color : "rgba(255,255,255,0.4)" } } > |</ span >
652- < span style = { { fontSize : 10 , color : "rgba(255,255,255,0.5)" } } > Your Vibe Coding Hub</ span >
689+ < div >
690+ < div style = { { fontSize : 13 , fontWeight : 700 , color : "#FFFFFF" } } > Lovcode</ div >
691+ < div style = { { fontSize : 9 , color : "rgba(255,255,255,0.5)" } } > Your Vibe Coding Hub</ div >
692+ </ div >
653693 </ div >
694+ < img
695+ src = "/lovcode-qrcode.png"
696+ alt = "QR Code"
697+ style = { { width : 48 , height : 48 , borderRadius : 4 , background : "#FFFFFF" , padding : 2 } }
698+ />
654699 </ div >
655700 </ div >
656701 </ div >
@@ -720,7 +765,7 @@ export function AnnualReport2025({ onClose }: AnnualReport2025Props) {
720765 ) ;
721766
722767 // Share functionality - uses domToCanvas with Portal (no UI flicker)
723- // Inspired by lovshot: clone DOM in memory, use scrollWidth/scrollHeight for original size
768+ // Uses Tauri save dialog for proper file saving without Finder popup
724769 const handleShare = useCallback ( async ( ) => {
725770 const captureEl = shareCardRef . current ;
726771 if ( ! captureEl || ! report ) return ;
@@ -734,7 +779,7 @@ export function AnnualReport2025({ onClose }: AnnualReport2025Props) {
734779 // domToCanvas clones DOM in memory - no UI impact
735780 const canvas = await domToCanvas ( captureEl , {
736781 scale : 2 ,
737- backgroundColor : "#F9F9F7 " ,
782+ backgroundColor : "#C54B24 " ,
738783 width : originalWidth ,
739784 height : originalHeight ,
740785 style : {
@@ -745,17 +790,32 @@ export function AnnualReport2025({ onClose }: AnnualReport2025Props) {
745790 } ,
746791 } ) ;
747792
748- // Convert canvas to blob and download
749- canvas . toBlob ( ( blob ) => {
750- if ( blob ) {
751- const url = URL . createObjectURL ( blob ) ;
752- const link = document . createElement ( "a" ) ;
753- link . download = "lovcode-2025-report.png" ;
754- link . href = url ;
755- link . click ( ) ;
756- URL . revokeObjectURL ( url ) ;
757- }
758- } , "image/png" ) ;
793+ // Convert canvas to blob
794+ const blob = await new Promise < Blob | null > ( ( resolve ) => {
795+ canvas . toBlob ( resolve , "image/png" ) ;
796+ } ) ;
797+
798+ if ( ! blob ) {
799+ throw new Error ( "Failed to create blob" ) ;
800+ }
801+
802+ // Use Tauri save dialog
803+ const path = await save ( {
804+ defaultPath : "lovcode-2025-report.png" ,
805+ filters : [ { name : "PNG Image" , extensions : [ "png" ] } ] ,
806+ } ) ;
807+
808+ if ( path ) {
809+ // Convert blob to Uint8Array
810+ const arrayBuffer = await blob . arrayBuffer ( ) ;
811+ const data = Array . from ( new Uint8Array ( arrayBuffer ) ) ;
812+
813+ // Write file using Tauri command
814+ await invoke ( "write_binary_file" , { path, data } ) ;
815+
816+ // Reveal in Finder
817+ await revealItemInDir ( path ) ;
818+ }
759819 } catch ( err ) {
760820 console . error ( "Failed to generate image:" , err ) ;
761821 } finally {
0 commit comments