@@ -618,26 +618,116 @@ ipcMainHandle("GET_ALT_PORT_INFOS", () => {
618
618
return engineManager . altPortInfo ;
619
619
} ) ;
620
620
621
+ /**
622
+ * 保存に適した場所を選択するかキャンセルするまでダイアログを繰り返し表示する。
623
+ * アンインストール等で消えうる場所などを避ける。
624
+ * @param showDialogFunction ダイアログを表示する関数
625
+ */
626
+ const retryShowSaveDialogWhileSafeDir = async <
627
+ T extends Electron . OpenDialogReturnValue | Electron . SaveDialogReturnValue
628
+ > (
629
+ showDialogFunction : ( ) => Promise < T >
630
+ ) : Promise < T > => {
631
+ /**
632
+ * 指定されたパスが安全でないかどうかを判断する
633
+ */
634
+ const isUnsafePath = ( filePath : string ) => {
635
+ const unsafeSaveDirs = [ appDirPath , app . getPath ( "userData" ) ] ; // アンインストールで消えうるフォルダ
636
+ return unsafeSaveDirs . some ( ( unsafeDir ) => {
637
+ const relativePath = path . relative ( unsafeDir , filePath ) ;
638
+ return ! (
639
+ path . isAbsolute ( relativePath ) ||
640
+ relativePath . startsWith ( `..${ path . sep } ` ) ||
641
+ relativePath === ".."
642
+ ) ;
643
+ } ) ;
644
+ } ;
645
+
646
+ /**
647
+ * 警告ダイアログを表示し、ユーザーが再試行を選択したかどうかを返す
648
+ */
649
+ const showWarningDialog = async ( ) => {
650
+ const productName = app . getName ( ) . toUpperCase ( ) ;
651
+ const warningResult = await dialog . showMessageBox ( win , {
652
+ message : `指定された保存先は${ productName } により自動的に削除される可能性があります。\n他の場所に保存することをおすすめします。` ,
653
+ type : "warning" ,
654
+ buttons : [ "保存場所を変更" , "無視して保存" ] ,
655
+ defaultId : 0 ,
656
+ title : "警告" ,
657
+ cancelId : 0 ,
658
+ } ) ;
659
+ return warningResult . response === 0 ? "retry" : "forceSave" ;
660
+ } ;
661
+
662
+ while ( true ) {
663
+ const result = await showDialogFunction ( ) ;
664
+ // キャンセルされた場合、結果を直ちに返す
665
+ if ( result . canceled ) return result ;
666
+
667
+ // 選択されたファイルパスを取得
668
+ const filePath =
669
+ "filePaths" in result ? result . filePaths [ 0 ] : result . filePath ;
670
+
671
+ // filePathが未定義の場合、エラーを返す
672
+ if ( filePath == undefined ) {
673
+ throw new Error (
674
+ `canseld == ${ result . canceled } but filePath == ${ filePath } `
675
+ ) ;
676
+ }
677
+
678
+ // 選択されたパスが安全かどうかを確認
679
+ if ( isUnsafePath ( filePath ) ) {
680
+ const result = await showWarningDialog ( ) ;
681
+ if ( result === "retry" ) continue ; // ユーザーが保存場所を変更を選択した場合
682
+ }
683
+ return result ; // 安全なパスが選択された場合
684
+ }
685
+ } ;
686
+
621
687
ipcMainHandle ( "SHOW_AUDIO_SAVE_DIALOG" , async ( _ , { title, defaultPath } ) => {
622
- const result = await dialog . showSaveDialog ( win , {
623
- title,
624
- defaultPath,
625
- filters : [ { name : "Wave File" , extensions : [ "wav" ] } ] ,
626
- properties : [ "createDirectory" ] ,
627
- } ) ;
688
+ const result = await retryShowSaveDialogWhileSafeDir ( ( ) =>
689
+ dialog . showSaveDialog ( win , {
690
+ title,
691
+ defaultPath,
692
+ filters : [ { name : "Wave File" , extensions : [ "wav" ] } ] ,
693
+ properties : [ "createDirectory" ] ,
694
+ } )
695
+ ) ;
628
696
return result . filePath ;
629
697
} ) ;
630
698
631
699
ipcMainHandle ( "SHOW_TEXT_SAVE_DIALOG" , async ( _ , { title, defaultPath } ) => {
632
- const result = await dialog . showSaveDialog ( win , {
633
- title,
634
- defaultPath,
635
- filters : [ { name : "Text File" , extensions : [ "txt" ] } ] ,
636
- properties : [ "createDirectory" ] ,
637
- } ) ;
700
+ const result = await retryShowSaveDialogWhileSafeDir ( ( ) =>
701
+ dialog . showSaveDialog ( win , {
702
+ title,
703
+ defaultPath,
704
+ filters : [ { name : "Text File" , extensions : [ "txt" ] } ] ,
705
+ properties : [ "createDirectory" ] ,
706
+ } )
707
+ ) ;
638
708
return result . filePath ;
639
709
} ) ;
640
710
711
+ /**
712
+ * 保存先になるディレクトリを選ぶダイアログを表示する。
713
+ */
714
+ ipcMainHandle ( "SHOW_SAVE_DIRECTORY_DIALOG" , async ( _ , { title } ) => {
715
+ const result = await retryShowSaveDialogWhileSafeDir ( ( ) =>
716
+ dialog . showOpenDialog ( win , {
717
+ title,
718
+ properties : [
719
+ "openDirectory" ,
720
+ "createDirectory" ,
721
+ "treatPackageAsDirectory" ,
722
+ ] ,
723
+ } )
724
+ ) ;
725
+ if ( result . canceled ) {
726
+ return undefined ;
727
+ }
728
+ return result . filePaths [ 0 ] ;
729
+ } ) ;
730
+
641
731
ipcMainHandle ( "SHOW_VVPP_OPEN_DIALOG" , async ( _ , { title, defaultPath } ) => {
642
732
const result = await dialog . showOpenDialog ( win , {
643
733
title,
@@ -650,6 +740,10 @@ ipcMainHandle("SHOW_VVPP_OPEN_DIALOG", async (_, { title, defaultPath }) => {
650
740
return result . filePaths [ 0 ] ;
651
741
} ) ;
652
742
743
+ /**
744
+ * ディレクトリ選択ダイアログを表示する。
745
+ * 保存先として選ぶ場合は SHOW_SAVE_DIRECTORY_DIALOG を使うべき。
746
+ */
653
747
ipcMainHandle ( "SHOW_OPEN_DIRECTORY_DIALOG" , async ( _ , { title } ) => {
654
748
const result = await dialog . showOpenDialog ( win , {
655
749
title,
@@ -662,12 +756,14 @@ ipcMainHandle("SHOW_OPEN_DIRECTORY_DIALOG", async (_, { title }) => {
662
756
} ) ;
663
757
664
758
ipcMainHandle ( "SHOW_PROJECT_SAVE_DIALOG" , async ( _ , { title, defaultPath } ) => {
665
- const result = await dialog . showSaveDialog ( win , {
666
- title,
667
- defaultPath,
668
- filters : [ { name : "VOICEVOX Project file" , extensions : [ "vvproj" ] } ] ,
669
- properties : [ "showOverwriteConfirmation" ] ,
670
- } ) ;
759
+ const result = await retryShowSaveDialogWhileSafeDir ( ( ) =>
760
+ dialog . showSaveDialog ( win , {
761
+ title,
762
+ defaultPath,
763
+ filters : [ { name : "VOICEVOX Project file" , extensions : [ "vvproj" ] } ] ,
764
+ properties : [ "showOverwriteConfirmation" ] ,
765
+ } )
766
+ ) ;
671
767
if ( result . canceled ) {
672
768
return undefined ;
673
769
}
0 commit comments