import { DatePipe } from '@angular/common' ; import { Injectable , OnDestroy } from '@angular/core' ; import { select , Store } from '@ngrx/store' ; import { shell } from 'electron' ; import * as path from 'path' ; import { Observable , Subject , Subscription , zip } from 'rxjs' ; import { map , switchMap , take } from 'rxjs/operators' ; import { makeNoteContentFileName , Note , NoteSnippetTypes } from '../../../core/note' ; import { VcsFileChange , VcsFileChangeStatusTypes } from '../../../core/vcs' ; import { isOutsidePath } from '../../../libs/path' ; import { toPromise } from '../../../libs/rx' ; import { uuid } from '../../../libs/uuid' ; import { FsService , WorkspaceService } from '../../shared' ; import { NoteContentFileAlreadyExistsError , NoteOutsideWorkspaceError } from '../note-errors' ; import { convertToNoteSnippets , NoteParser } from '../note-shared' ; import { NoteStateWithRoot } from '../note.state' ; import { AddNoteAction , ChangeNoteStacksAction , ChangeNoteTitleAction , DeleteNoteAction , DeselectNoteAction , LoadNoteCollectionAction , LoadNoteCollectionCompleteAction , SelectNoteAction , } from './note-collection.actions' ; import { NoteItem } from './note-item.model' ; @ Injectable ( ) export class NoteCollectionService implements OnDestroy { private toggleNoteSelectionSubscription = Subscription . EMPTY ; private readonly _toggleNoteSelection = new Subject < NoteItem > ( ) ; private vcsFileChanges : VcsFileChange [ ] = [ ] ; private vcsFileChangesSubscription = Subscription . EMPTY ; constructor ( private store , private parser , private fs , private workspace , private datePipe ) { this . subscribeToggles ( ) ; } ngOnDestroy ( ) { this . toggleNoteSelectionSubscription . unsubscribe ( ) ; this . vcsFileChangesSubscription . unsubscribe ( ) ; } provideVcsFileChanges ( fileChanges ) { this . vcsFileChangesSubscription = fileChanges . subscribe ( changes => this . vcsFileChanges = changes ) ; } returns { Promise < void > } async loadOnce ( ) { const notesDirPath = this . workspace . configs . notesDirPath ; this . store . dispatch ( new LoadNoteCollectionAction ( ) ) ; const noteFileNames = await toPromise ( this . fs . readDirectory ( notesDirPath ) ) ; const filteredNoteFileNames = noteFileNames . filter ( fileName => path . extname ( fileName ) === '.json' ) ; const readingNotes = [ ] ; filteredNoteFileNames . forEach ( ( fileName ) => { const filePath = path . resolve ( notesDirPath , fileName ) ; readingNotes . push ( toPromise ( this . fs . readJsonFile < Note > ( filePath ) ) ) ; } ) ; const results = await Promise . all ( readingNotes ) ; const notes = results . filter ( note => note !== null ) . map ( ( note , index ) => ( { ... note , fileName : filteredNoteFileNames [ index ] , filePath : path . resolve ( notesDirPath , filteredNoteFileNames [ index ] ) , contentFilePath : note . label ? path . resolve ( this . workspace . configs . rootDirPath , note . label , note . contentFileName ) : path . resolve ( this . workspace . configs . rootDirPath , note . contentFileName ) , } ) ) ; this . store . dispatch ( new LoadNoteCollectionCompleteAction ( { notes } ) ) ; } getFilteredAndSortedNoteList ( ) { return this . store . pipe ( select ( state => state . note . collection . filteredAndSortedNotes ) ) ; } getSelectedNote ( ) { return this . store . pipe ( select ( state => state . note . collection . selectedNote ) ) ; } getNoteVcsFileChanges ( note ) { const { contentFilePath , filePath } = note ; const matchedFilePaths = [ contentFilePath , filePath ] ; return this . vcsFileChanges . filter ( change => matchedFilePaths . includes ( change . absoluteFilePath ) ) ; } getNoteVcsFileChangeStatus ( note ) { const fileChanges = this . getNoteVcsFileChanges ( note ) ; return fileChanges . length ? fileChanges [ 0 ] . status : null ; } toggleNoteSelection ( note ) { this . _toggleNoteSelection . next ( note ) ; } selectNote ( note ) { this . store . dispatch ( new SelectNoteAction ( { note } ) ) ; } deselectNote ( ) { this . store . dispatch ( new DeselectNoteAction ( ) ) ; } async createNewNote ( title , directory = '' ) { const rootDirPath = this . workspace . configs . rootDirPath ; const createdAt = new Date ( ) . getTime ( ) ; const contentFileName = makeNoteContentFileName ( createdAt , title ) ; const contentFilePath = path . resolve ( rootDirPath , directory , contentFileName ) ; if ( isOutsidePath ( contentFilePath , rootDirPath ) ) { throw new NoteOutsideWorkspaceError ( ) ; } if ( await toPromise ( this . fs . isPathExists ( contentFilePath ) ) ) { throw new NoteContentFileAlreadyExistsError ( ) ; } const content = { snippets : [ { type : NoteSnippetTypes . TEXT , value : 'Write some content...' , } , ] , } ; const result = this . parser . parseNoteContent ( content , { metadata : { title , date : this . datePipe . transform ( new Date ( ) , 'E, d MMM yyyy HH:mm:ss Z' ) , stacks : [ ] , } , } ) ; const id = uuid ( ) ; const noteFileName = `${ id }.json` ; const noteFilePath = path . resolve ( this . workspace . configs . notesDirPath , noteFileName ) ; const contentRawValue = result . contentRawValue ; const note = { id , title , snippets : convertToNoteSnippets ( result . parsedSnippets ) , createdDatetime : createdAt , stackIds : [ ] , label : directory , contentFileName , } ; /**
         * Make sure to ensure the directory where content file will saved.
         */ await toPromise ( this . fs . ensureDirectory ( path . dirname ( contentFilePath ) ) ) ; await toPromise ( zip ( this . fs . writeJsonFile < Note > ( noteFilePath , note ) , this . fs . writeFile ( contentFilePath , contentRawValue ) ) ) ; const noteItem = { ... note , fileName : noteFileName , filePath : noteFilePath , contentFilePath , } ; this . store . dispatch ( new AddNoteAction ( { note : noteItem } ) ) ; } async changeNoteTitle ( noteItem , newTitle ) { const dirName = path . dirname ( noteItem . contentFilePath ) ; let newContentFileName ; let newContentFilePath ; const allNotes = await toPromise ( this . store . pipe ( select ( state => state . note . collection . notes ) , take ( 1 ) ) ) ; const allNoteContentFilePaths = allNotes . map ( item => item . contentFilePath ) ; let index = 0 ; const isNoteTitleDuplicated = title => allNoteContentFilePaths . includes ( title ) ; do { const title = index === 0 ? newTitle : `${ newTitle }(${ index })` ; newContentFileName = makeNoteContentFileName ( noteItem . createdDatetime , title ) ; newContentFilePath = path . resolve ( dirName , newContentFileName ) ; index ++ ; } while ( isNoteTitleDuplicated ( newContentFilePath ) ) ; if ( newContentFileName === noteItem . contentFileName ) { return ; } try { await toPromise ( this . fs . renameFile ( noteItem . contentFilePath , newContentFilePath ) ) ; } catch ( error ) { throw new NoteContentFileAlreadyExistsError ( ) ; } this . store . dispatch ( new ChangeNoteTitleAction ( { note : noteItem , title : newTitle , contentFileName : newContentFileName , contentFilePath : newContentFilePath , } ) ) ; } changeNoteStacks ( noteItem , stacks ) { this . store . dispatch ( new ChangeNoteStacksAction ( { note : noteItem , stacks } ) ) ; } deleteNote ( noteItem ) { const allRemoved = shell . moveItemToTrash ( noteItem . filePath ) && shell . moveItemToTrash ( noteItem . contentFilePath ) ; if ( ! allRemoved ) { return ; } this . getSelectedNote ( ) . pipe ( take ( 1 ) ) . subscribe ( ( selectedNote ) => { if ( selectedNote && selectedNote . id === noteItem . id ) { this . store . dispatch ( new DeselectNoteAction ( ) ) ; } this . store . dispatch ( new DeleteNoteAction ( { note : noteItem } ) ) ; } ) ; } private subscribeToggles ( ) { this . toggleNoteSelectionSubscription = this . _toggleNoteSelection . asObservable ( ) . pipe ( switchMap ( note => this . getSelectedNote ( ) . pipe ( take ( 1 ) , map ( selectedNote => ( [ selectedNote , note ] ) ) ) ) ) . subscribe ( ( [ selectedNote , note ] ) => this . handleNoteSelectionToggling ( selectedNote , note ) ) ; } private handleNoteSelectionToggling ( selectedNote , note ) { if ( selectedNote && selectedNote . id === note . id ) { this . deselectNote ( ) ; } else { this . selectNote ( note ) ; } } }