license import { Button , Classes , Colors , ControlGroup , Dialog , Icon , InputGroup , Intent , } from "@blueprintjs/core" ; import { css , StyleSheet } from "aphrodite" ; import React from "react" ; import { Auth } from "./auth" ; import { Conflict , File , FileNotFound , ls , rm , write } from "./gitfs" ; import ModalSaving from "./ModalSaving" ; import RPCClient from "./RPCClient" ; interface Props { auth : Auth ; code : string ; rpc : RPCClient ; onHide ( ) : void ; onPublished ( path ) : void ; } interface State { filename : string ; files : File [ ] | null ; invitationRequired : boolean ; saving : boolean ; } /**
 * This is the modal you see when you try to save an untitled file.
 * It asks you for a name.
 */ class ModalPublish extends React . PureComponent < Props , State > { state : State = { filename : "" , files : null , invitationRequired : false , saving : false , } ; componentDidMount ( ) { this . loadExistingSongs ( ) ; } render ( ) { const { auth , onHide } = this . props ; const { filename , files , invitationRequired , saving } = this . state ; let disabled = false ; if ( saving ) { return < ModalSaving />; } let error = null ; if ( ! filename . length ) { disabled = true ; error = ( < span className = { css ( styles . error ) } > < Icon icon = "error" / > Please enter a filename . < /span>); } else if ( files && files . map ( ( file ) => file . path ) . indexOf ( `${ filename }.ly` ) !== - 1 ) { disabled = true ; error = ( < span className = { css ( styles . error ) } > < Icon icon = "error" / > That filename is taken . < /span>); } else if ( invitationRequired ) { error = ( < span className = { css ( styles . error ) } > Permission denied . You may need to { " " } < a href = { `https://github.com/${ auth . repo }/invitations` } target = "_blank" rel = "noreferrer noopener" > enable write access < /a>{" "} then try agin ! < /span>); } return ( < Dialog title = "Save song" isOpen = { true } onClose = { onHide } className = { css ( styles . modal ) } > < div className = { Classes . DIALOG_BODY } > < ControlGroup > < div className = { css ( styles . prefix ) } > Save to : & nbsp ; < code className = { Classes . MONOSPACE_TEXT } > < a href = { `https://github.com/${ auth . repo }` } target = "_blank" rel = "noopener noreferrer" > { `${ auth . repo }` } < /a> / < /code> < /div> < InputGroup value = { filename } className = { css ( styles . input ) } placeholder = "filename" autoFocus = { true } onChange = { this . handleChange } rightElement = { < div className = { css ( styles . suffix ) } > < code className = { Classes . MONOSPACE_TEXT } > & nbsp ; . ly < /code> < /div>}/ > < /ControlGroup> < /div> < div className = { Classes . DIALOG_FOOTER } > < div className = { Classes . DIALOG_FOOTER_ACTIONS } > { error } < Button onClick = { this . handleSave } disabled = { disabled } intent = { Intent . PRIMARY } icon = "floppy-disk" > Save < /Button> < /div> < /div> < /Dialog>); } private handleChange = ( ev ) : void => { const filename = ev . target . value . replace ( /[^a-zA-Z0-9_-]*/g , "" ) ; this . setState ( { filename , } ) ; } ; private handleSave = async ( ) : Promise < void > => { let didFail = false ; try { this . setState ( { saving : true , } ) ; const { code , auth , rpc } = this . props ; const { filename } = this . state ; await doPublish ( code , auth , `${ filename }.ly` , rpc , false ) ; } catch ( err ) { if ( err instanceof FileNotFound ) { didFail = true ; this . setState ( { invitationRequired : true , } ) ; return ; } console . log ( err ) ; alert ( err . toString ( ) ) ; didFail = true ; } finally { this . setState ( { saving : false , } ) ; if ( ! didFail ) { this . props . onPublished ( `${ this . props . auth . repo }/${ this . state . filename }.ly` ) ; } } } ; private loadExistingSongs = async ( ) : Promise < void > => { const { auth : { accessToken , repo } , } = this . props ; const files = await ls ( accessToken , repo ) ; this . setState ( { files , } ) ; } ; } function b64EncodeUnicode ( str ) { return btoa ( encodeURIComponent ( str ) . replace ( /%([0-9A-F]{2})/g , ( _match , p1 ) => String . fromCharCode ( + `0x${ p1 }` ) ) ) ; } export async function doPublish ( code , auth , filename , rpc , overwrite ) { let version = "stable" ; const maybeVersion = /\\version\s*"(\d+)\.?(\d+)?\.?(\d+)?/gm . exec ( code ) ; const versionSlices = maybeVersion ? maybeVersion . slice ( 1 ) . map ( ( v ) => parseInt ( v , 10 ) ) : [ ] ; const isUnstable = versionSlices [ 0 ] === 2 && versionSlices [ 1 ] > 22 ; version = isUnstable ? "unstable" : "stable" ; const pdf = ( await rpc . call ( "render" , { version , backend : "pdf" , src : code , } ) ) . result . files [ 0 ] ; const pdfFilename = filename . replace ( /\.ly$/ , ".pdf" ) ; const files = await ls ( auth . accessToken , auth . repo ) ; const file = files . find ( ( candidate ) => candidate . path === filename ) ; const pdfFile = files . find ( ( candidate ) => candidate . path === pdfFilename ) ; if ( ! overwrite && ( file || pdfFile ) ) { throw new Error ( "That name is already taken." ) ; } const { accessToken , repo } = auth ; try { await write ( accessToken , repo , pdfFilename , pdf , pdfFile ? pdfFile . sha : undefined ) ; await write ( accessToken , repo , filename , b64EncodeUnicode ( code ) , file ? file . sha : undefined ) ; } catch ( err ) { console . log ( err ) ; if ( err instanceof Conflict ) { if ( overwrite ) { throw new Error ( "Could not save file. " + "You may need to wait a minute or so after publishing before publishing again." ) ; } else { throw new Error ( "This name is already taken." ) ; } } else if ( err instanceof FileNotFound ) { throw err ; } else { throw new Error ( "Could not save file." ) ; } } return true ; } export async function doUnpublish ( auth , filename , _rpc ) { const pdfFilename = filename . replace ( /\.ly$/ , ".pdf" ) ; const files = await ls ( auth . accessToken , auth . repo ) ; const file = files . find ( ( candidate ) => candidate . path === filename ) ; const pdfFile = files . find ( ( candidate ) => candidate . path === pdfFilename ) ; const { accessToken , repo } = auth ; try { if ( pdfFile ) { await rm ( accessToken , repo , pdfFilename , pdfFile . sha ) ; } if ( file ) { await rm ( accessToken , repo , filename , file . sha ) ; } } catch ( err ) { console . log ( err ) ; if ( err instanceof Conflict ) { alert ( "Could not delete file." ) ; return false ; } alert ( "Could not delete file." ) ; return false ; } return true ; } export default ModalPublish ; const styles = StyleSheet . create ( { prefix : { alignSelf : "center" , marginRight : 4 , } , error : { color : Colors . RED1 , flex : 1 , alignSelf : "center" , } , mono : { fontFamily : "monospace" , } , modal : { width : 565 , } , input : { fontFamily : "monospace" , display : "flex" , flex : 1 , } , suffix : { height : 30 , lineHeight : "30px" , marginRight : 7 , } , } ) ;