import { Injectable , OnDestroy , NgZone } from '@angular/core' ; import { AbstractControl , ValidatorFn } from '@angular/forms' ; import * as appSettings from '@nativescript/core/application-settings' ; import { File , Folder } from '@nativescript/core/file-system' ; import { isAndroid } from '@nativescript/core/platform' ; import { Subject , Subscription , interval } from 'rxjs' ; import * as permissions from 'nativescript-permissions' ; import { BEANCOUNT_PATH_SETTING } from './constants' ; import { BeancountFileContent } from './beancount-file-content' ; const BEANCOUNT_FILE_WATCH_INTERVAL = 60 * 1000 ; @ Injectable ( { providedIn : 'root' , } ) export class BeancountFileService implements OnDestroy { path : string ; content : BeancountFileContent ; contentStream : Subject < BeancountFileContent > ; watcher : Subscription ; isLoading = false ; constructor ( private zone ) { this . path = appSettings . getString ( BEANCOUNT_PATH_SETTING ) ; this . contentStream = new Subject ( ) ; if ( this . path ) { this . load ( ) ; } this . watcher = interval ( BEANCOUNT_FILE_WATCH_INTERVAL ) . subscribe ( ( ) => { if ( ! this . path ) { return ; } this . watcherLoad ( ) ; } ) ; } static isValidPath ( path ) { if ( path && File . exists ( path ) && ! Folder . exists ( path ) ) { return true ; } return false ; } setPath ( path ) { if ( ! path ) { throw Error ( 'Path can not be empty.' ) ; } appSettings . setString ( BEANCOUNT_PATH_SETTING , path ) ; this . path = path ; this . clearCache ( ) ; } private async checkPermission ( ) { let hasPermission = true ; if ( isAndroid ) { const permissionName = 'android.permission.WRITE_EXTERNAL_STORAGE' ; hasPermission = permissions . hasPermission ( permissionName ) ; if ( ! hasPermission ) { try { const result = await permissions . requestPermission ( permissionName , 'Your permission is required.' ) ; hasPermission = result [ permissionName ] ; } catch ( error ) { console . warn ( 'permission denied' ) ; hasPermission = false ; } } } return hasPermission ; } private clearCache ( ) { delete this . content ; } private async read ( ) { this . isLoading = true ; let fileText ; const hasPermission = await this . checkPermission ( ) ; if ( hasPermission ) { const file = File . fromPath ( this . path ) ; fileText = await file . readText ( ) ; if ( fileText . charCodeAt ( fileText . length - 1 ) !== 10 ) { fileText += '\n' ; } } else { fileText = '' ; } this . zone . run ( ( ) => { this . isLoading = false ; } ) ; return fileText ; } async load ( force = false ) { if ( force || this . content === undefined ) { const fileText = await this . read ( ) ; this . content = new BeancountFileContent ( fileText ) ; } this . contentStream . next ( this . content ) ; return this . content ; } private async watcherLoad ( ) { const fileText = await this . read ( ) ; if ( ! this . content || this . content . text !== fileText ) { this . content = new BeancountFileContent ( fileText ) ; this . contentStream . next ( this . content ) ; } } append ( text ) { this . content . append ( text ) ; this . save ( ) ; } async save ( ) { const file = File . fromPath ( this . path ) ; try { await file . writeText ( this . content . text ) ; } catch ( error ) { console . warn ( 'file not saved:' , error ) ; } } reset ( ) { appSettings . remove ( BEANCOUNT_PATH_SETTING ) ; delete this . path ; this . clearCache ( ) ; } ngOnDestroy ( ) { this . watcher . unsubscribe ( ) ; } } export function BeancountFilePathValidator ( ) { return ( control ) : { [ key ] : any ; } | null => { const path = control . value ; if ( ! BeancountFileService . isValidPath ( path ) ) { return { invalidPath : { value : control . value , } , } ; } else { return null ; } } ; }