package ui
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/RasmusLindroth/go-mastodon"
"github.com/RasmusLindroth/tut/api"
"github.com/RasmusLindroth/tut/config"
"github.com/RasmusLindroth/tut/util"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
"github.com/rivo/uniseg"
"mvdan.cc/xurls/v2"
)
type msgToot struct {
ID mastodon . ID
Text string
Reply * mastodon . Status
Edit * mastodon . Status
MediaIDs [ ] mastodon . ID
Sensitive bool
CWText string
ScheduledAt * time . Time
QuoteIncluded bool
Visibility string
Language string
}
type ComposeView struct {
tutView * TutView
shared * Shared
View * tview . Flex
content * tview . TextView
textAreaMain * tview . TextArea
textAreaCW * tview . TextArea
input * MediaInput
info * tview . TextView
controls * tview . Flex
visibility * tview . DropDown
lang * tview . DropDown
media * MediaList
msg * msgToot
}
var visibilities = map [ string ] int {
mastodon . VisibilityPublic : 0 ,
mastodon . VisibilityUnlisted : 1 ,
mastodon . VisibilityFollowersOnly : 2 ,
mastodon . VisibilityDirectMessage : 3 ,
}
var visibilitiesStr = [ ] string {
mastodon . VisibilityPublic ,
mastodon . VisibilityUnlisted ,
mastodon . VisibilityFollowersOnly ,
mastodon . VisibilityDirectMessage ,
}
func NewComposeView ( tv * TutView ) * ComposeView {
cv := & ComposeView {
tutView : tv ,
shared : tv . Shared ,
content : NewTextView ( tv . tut . Config ) ,
textAreaMain : NewTextArea ( tv . tut . Config ) ,
textAreaCW : NewTextArea ( tv . tut . Config ) ,
input : NewMediaInput ( tv ) ,
controls : NewControlView ( tv . tut . Config ) ,
info : NewTextView ( tv . tut . Config ) ,
visibility : NewDropDown ( tv . tut . Config ) ,
lang : NewDropDown ( tv . tut . Config ) ,
media : NewMediaList ( tv ) ,
}
cv . content . SetDynamicColors ( true )
cv . View = newComposeUI ( cv )
return cv
}
func newComposeUI ( cv * ComposeView ) * tview . Flex {
r := tview . NewFlex ( ) . SetDirection ( tview . FlexRow )
if cv . tutView . tut . Config . General . TerminalTitle < 2 {
r . AddItem ( cv . tutView . Shared . Top . View , 1 , 0 , false )
}
if ! cv . tutView . tut . Config . General . UseInternalEditor {
r . AddItem ( tview . NewFlex ( ) . SetDirection ( tview . FlexColumn ) .
AddItem ( tview . NewFlex ( ) . SetDirection ( tview . FlexRow ) .
AddItem ( cv . content , 0 , 2 , false ) , 0 , 2 , false ) .
AddItem ( tview . NewBox ( ) , 2 , 0 , false ) .
AddItem ( tview . NewFlex ( ) . SetDirection ( tview . FlexRow ) .
AddItem ( cv . visibility , 1 , 0 , false ) .
AddItem ( cv . lang , 1 , 0 , false ) .
AddItem ( cv . info , 5 , 0 , false ) .
AddItem ( cv . media . View , 0 , 1 , false ) , 0 , 1 , false ) , 0 , 1 , false ) .
AddItem ( cv . input . View , 1 , 0 , false ) .
AddItem ( cv . controls , 1 , 0 , false ) .
AddItem ( cv . tutView . Shared . Bottom . View , 2 , 0 , false )
} else {
txtOne := NewTextView ( cv . tutView . tut . Config )
txtOne . SetText ( "Content warning text:" )
txtTwo := NewTextView ( cv . tutView . tut . Config )
txtTwo . SetText ( "Main content:" )
r . AddItem ( tview . NewFlex ( ) . SetDirection ( tview . FlexColumn ) .
AddItem ( tview . NewFlex ( ) . SetDirection ( tview . FlexRow ) .
AddItem ( cv . content , 3 , 0 , false ) .
AddItem ( txtOne , 1 , 0 , false ) .
AddItem ( cv . textAreaCW , 0 , 1 , false ) .
AddItem ( txtTwo , 1 , 0 , false ) .
AddItem ( cv . textAreaMain , 0 , 2 , false ) , 0 , 2 , false ) .
AddItem ( tview . NewBox ( ) , 2 , 0 , false ) .
AddItem ( tview . NewFlex ( ) . SetDirection ( tview . FlexRow ) .
AddItem ( cv . visibility , 1 , 0 , false ) .
AddItem ( cv . lang , 1 , 0 , false ) .
AddItem ( cv . info , 5 , 0 , false ) .
AddItem ( cv . media . View , 0 , 1 , false ) , 0 , 1 , false ) , 0 , 1 , false ) .
AddItem ( cv . input . View , 1 , 0 , false ) .
AddItem ( cv . controls , 1 , 0 , false ) .
AddItem ( cv . tutView . Shared . Bottom . View , 2 , 0 , false )
}
return r
}
type ComposeControls uint
const (
ComposeNormal ComposeControls = iota
ComposeMedia
)
func urlsInText ( txt string ) ( count , length int ) {
x := xurls . Strict ( )
matches := x . FindAllString ( txt , - 1 )
count = len ( matches )
for _ , m := range matches {
length += len ( m )
}
return
}
func ( cv * ComposeView ) msgLength ( ) int {
m := cv . msg
charCount := uniseg . GraphemeClusterCount ( m . Text )
spoilerCount := uniseg . GraphemeClusterCount ( m . CWText )
totalCount := charCount
urlLength := cv . tutView . tut . Client . GetLengthURL ( )
urls , length := urlsInText ( m . Text )
if urls > 0 {
totalCount = totalCount - length
totalCount = totalCount + ( urls * urlLength )
}
if m . Sensitive {
totalCount += spoilerCount
}
charsLeft := cv . tutView . tut . Client . GetCharLimit ( ) - totalCount
return charsLeft
}
func ( cv * ComposeView ) SetControls ( ctrl ComposeControls ) {
var items [ ] Control
switch ctrl {
case ComposeNormal :
items = append ( items , NewControl ( cv . tutView . tut . Config , cv . tutView . tut . Config . Input . ComposePost , true ) )
items = append ( items , NewControl ( cv . tutView . tut . Config , cv . tutView . tut . Config . Input . ComposeEditText , true ) )
items = append ( items , NewControl ( cv . tutView . tut . Config , cv . tutView . tut . Config . Input . ComposeVisibility , true ) )
items = append ( items , NewControl ( cv . tutView . tut . Config , cv . tutView . tut . Config . Input . ComposeToggleContentWarning , true ) )
items = append ( items , NewControl ( cv . tutView . tut . Config , cv . tutView . tut . Config . Input . ComposeEditCW , true ) )
items = append ( items , NewControl ( cv . tutView . tut . Config , cv . tutView . tut . Config . Input . ComposeMediaFocus , true ) )
items = append ( items , NewControl ( cv . tutView . tut . Config , cv . tutView . tut . Config . Input . ComposePoll , true ) )
items = append ( items , NewControl ( cv . tutView . tut . Config , cv . tutView . tut . Config . Input . ComposeLanguage , true ) )
if cv . msg . Reply != nil {
items = append ( items , NewControl ( cv . tutView . tut . Config , cv . tutView . tut . Config . Input . ComposeIncludeQuote , true ) )
}
case ComposeMedia :
items = append ( items , NewControl ( cv . tutView . tut . Config , cv . tutView . tut . Config . Input . MediaAdd , true ) )
items = append ( items , NewControl ( cv . tutView . tut . Config , cv . tutView . tut . Config . Input . MediaDelete , true ) )
items = append ( items , NewControl ( cv . tutView . tut . Config , cv . tutView . tut . Config . Input . MediaEditDesc , true ) )
items = append ( items , NewControl ( cv . tutView . tut . Config , cv . tutView . tut . Config . Input . GlobalBack , true ) )
}
cv . controls . Clear ( )
for i , item := range items {
if i < len ( items ) - 1 {
cv . controls . AddItem ( NewControlButton ( cv . tutView , item ) , item . Len + 1 , 0 , false )
} else {
cv . controls . AddItem ( NewControlButton ( cv . tutView , item ) , item . Len , 0 , false )
}
}
}
func ( cv * ComposeView ) SetStatus ( reply * mastodon . Status , edit * mastodon . Status ) error {
cv . tutView . PollView . Reset ( )
cv . media . Reset ( )
cv . textAreaMain . SetText ( "" , false )
cv . textAreaCW . SetText ( "" , false )
msg := & msgToot { }
me := cv . tutView . tut . Client . Me
visibility := mastodon . VisibilityPublic
lang := ""
if me . Source != nil && me . Source . Privacy != nil {
visibility = * me . Source . Privacy
}
if me . Source != nil && me . Source . Language != nil {
lang = * me . Source . Language
}
if reply != nil {
if reply . Reblog != nil {
reply = reply . Reblog
}
msg . Reply = reply
if reply . Sensitive {
msg . Sensitive = true
msg . CWText = reply . SpoilerText
}
if visibilities [ reply . Visibility ] > visibilities [ visibility ] {
visibility = reply . Visibility
}
}
msg . Visibility = visibility
msg . Language = lang
cv . msg = msg
cv . msg . Text = cv . getAccs ( )
if edit != nil {
source , err := cv . tutView . tut . Client . Client . GetStatusSource ( context . Background ( ) , edit . ID )
if err != nil {
cv . tutView . ShowError (
fmt . Sprintf ( "Couldn't get status. Error: %v\n" , err ) ,
)
return err
}
msg := & msgToot { }
msg . Edit = edit
msg . ID = source . ID
msg . Text = source . Text
msg . CWText = source . SpoilerText
for _ , mid := range edit . MediaAttachments {
msg . MediaIDs = append ( msg . MediaIDs , mid . ID )
}
msg . Sensitive = edit . Sensitive
msg . Visibility = edit . Visibility
msg . Language = edit . Language
if edit . Poll != nil {
cv . tutView . PollView . AddPoll ( edit . Poll )
}
if len ( edit . MediaAttachments ) > 0 {
cv . media . AddFromEdit ( edit )
}
cv . msg = msg
}
if cv . tutView . tut . Config . General . QuoteReply && edit == nil {
cv . IncludeQuote ( )
}
cv . visibility . SetLabel ( "Visibility: " )
index := 0
for i , v := range visibilitiesStr {
if cv . msg . Visibility == v {
index = i
break
}
}
cv . visibility . SetOptions ( visibilitiesStr , cv . visibilitySelected )
cv . visibility . SetCurrentOption ( index )
cv . visibility . SetInputCapture ( cv . visibilityInput )
cv . lang . SetLabel ( "Lang: " )
langStrs := [ ] string { }
for i , l := range util . Languages {
if cv . msg . Language == l . Code {
index = i
}
langStrs = append ( langStrs , fmt . Sprintf ( "%s (%s)" , l . Local , l . English ) )
}
cv . lang . SetOptions ( langStrs , cv . langSelected )
cv . lang . SetCurrentOption ( index )
if cv . tutView . tut . Config . General . UseInternalEditor {
cv . textAreaMain . SetText ( cv . msg . Text , true )
cv . textAreaCW . SetText ( cv . msg . CWText , true )
}
cv . UpdateContent ( )
cv . SetControls ( ComposeNormal )
return nil
}
func ( cv * ComposeView ) getAccs ( ) string {
if cv . msg . Reply == nil {
return ""
}
s := cv . msg . Reply
var users [ ] string
if s . Account . Acct != cv . tutView . tut . Client . Me . Acct {
users = append ( users , "@" + s . Account . Acct )
}
for _ , men := range s . Mentions {
if men . Acct == cv . tutView . tut . Client . Me . Acct {
continue
}
users = append ( users , "@" + men . Acct )
}
t := strings . Join ( users , " " )
return t
}
func ( cv * ComposeView ) EditText ( ) {
if ! cv . tutView . tut . Config . General . UseInternalEditor {
text , err := OpenEditor ( cv . tutView , cv . msg . Text )
if err != nil {
cv . tutView . ShowError (
fmt . Sprintf ( "Couldn't open editor. Error: %v" , err ) ,
)
return
}
cv . msg . Text = text
cv . UpdateContent ( )
} else {
cv . textAreaMainFocus ( )
}
}
func ( cv * ComposeView ) EditSpoiler ( ) {
if ! cv . tutView . tut . Config . General . UseInternalEditor {
text , err := OpenEditor ( cv . tutView , cv . msg . CWText )
if err != nil {
cv . tutView . ShowError (
fmt . Sprintf ( "Couldn't open editor. Error: %v" , err ) ,
)
return
}
cv . msg . CWText = text
cv . UpdateContent ( )
} else {
cv . textAreaCWFocus ( )
}
}
func ( cv * ComposeView ) ToggleCW ( ) {
cv . msg . Sensitive = ! cv . msg . Sensitive
cv . UpdateContent ( )
}
func ( cv * ComposeView ) UpdateContent ( ) {
cv . info . SetText ( fmt . Sprintf ( "Chars left: %d\nCW: %t\nHas poll: %t\n" , cv . msgLength ( ) , cv . msg . Sensitive , cv . tutView . PollView . HasPoll ( ) ) )
normal := config . ColorMark ( cv . tutView . tut . Config . Style . Text )
subtleColor := config . ColorMark ( cv . tutView . tut . Config . Style . Subtle )
warningColor := config . ColorMark ( cv . tutView . tut . Config . Style . WarningText )
var outputHead string
var output string
if cv . msg . Reply != nil {
var acct string
if cv . msg . Reply . Account . DisplayName != "" {
acct = fmt . Sprintf ( "%s (%s)" , cv . msg . Reply . Account . DisplayName , cv . msg . Reply . Account . Acct )
} else {
acct = cv . msg . Reply . Account . Acct
}
outputHead += subtleColor + "Replying to " + tview . Escape ( acct ) + "\n" + normal
}
if cv . msg . CWText != "" && ! cv . msg . Sensitive {
outputHead += warningColor + "You have entered content warning text, but haven't set an content warning. Do it by pressing " + tview . Escape ( "[T]" ) + "\n\n" + normal
}
if cv . msg . Sensitive && cv . msg . CWText == "" {
outputHead += warningColor + "You have added an content warning, but haven't set any text above the hidden text. Do it by pressing " + tview . Escape ( "[C]" ) + "\n\n" + normal
}
if ! cv . tutView . tut . Config . General . UseInternalEditor {
if cv . msg . Sensitive && cv . msg . CWText != "" {
outputHead += subtleColor + "Content warning\n\n" + normal
outputHead += tview . Escape ( cv . msg . CWText )
outputHead += "\n\n" + subtleColor + "---hidden content below---\n\n" + normal
}
output = outputHead + normal + tview . Escape ( cv . msg . Text )
} else {
output = strings . TrimSpace ( outputHead )
}
cv . content . SetText ( output )
}
func ( cv * ComposeView ) IncludeQuote ( ) {
if cv . msg . QuoteIncluded {
return
}
t := cv . msg . Text
s := cv . msg . Reply
if s == nil {
return
}
tootText , _ := util . CleanHTML ( s . Content )
t += "\n\n"
for _ , line := range strings . Split ( tootText , "\n" ) {
t += "> " + line + "\n"
}
cv . msg . Text = t
if cv . tutView . tut . Config . General . UseInternalEditor {
cv . textAreaMain . SetText ( cv . msg . Text , false )
}
cv . msg . QuoteIncluded = true
cv . UpdateContent ( )
}
func ( cv * ComposeView ) textAreaInput ( event * tcell . EventKey ) * tcell . EventKey {
if cv . tutView . tut . Config . Input . GlobalBack . Match ( event . Key ( ) , rune ( - 1 ) ) {
cv . exitTextAreaInput ( )
return nil
}
switch key := event . Key ( ) ; key {
case tcell . KeyCtrlQ :
return nil
case tcell . KeyCtrlC :
return tcell . NewEventKey ( tcell . KeyCtrlQ , rune ( 0 ) , tcell . ModNone )
}
return event
}
func ( cv * ComposeView ) exitTextAreaInput ( ) {
cv . tutView . tut . App . SetInputCapture ( cv . tutView . Input )
cv . tutView . tut . App . SetFocus ( cv . content )
}
func ( cv * ComposeView ) textAreaMainFocus ( ) {
cv . tutView . tut . App . SetInputCapture ( cv . textAreaInput )
cv . textAreaMain . SetChangedFunc ( func ( ) {
cv . msg . Text = cv . textAreaMain . GetText ( )
cv . UpdateContent ( )
} )
cv . tutView . tut . App . SetFocus ( cv . textAreaMain )
}
func ( cv * ComposeView ) textAreaCWFocus ( ) {
cv . tutView . tut . App . SetInputCapture ( cv . textAreaInput )
cv . textAreaCW . SetChangedFunc ( func ( ) {
cv . msg . CWText = cv . textAreaCW . GetText ( )
cv . UpdateContent ( )
} )
cv . tutView . tut . App . SetFocus ( cv . textAreaCW )
}
func ( cv * ComposeView ) HasMedia ( ) bool {
return len ( cv . media . Files ) > 0
}
func ( cv * ComposeView ) visibilityInput ( event * tcell . EventKey ) * tcell . EventKey {
if cv . tutView . tut . Config . Input . GlobalDown . Match ( event . Key ( ) , event . Rune ( ) ) {
return tcell . NewEventKey ( tcell . KeyDown , 0 , tcell . ModNone )
}
if cv . tutView . tut . Config . Input . GlobalUp . Match ( event . Key ( ) , event . Rune ( ) ) {
return tcell . NewEventKey ( tcell . KeyUp , 0 , tcell . ModNone )
}
if cv . tutView . tut . Config . Input . GlobalExit . Match ( event . Key ( ) , event . Rune ( ) ) ||
cv . tutView . tut . Config . Input . GlobalBack . Match ( event . Key ( ) , event . Rune ( ) ) {
cv . exitVisibility ( )
return nil
}
return event
}
func ( cv * ComposeView ) exitVisibility ( ) {
cv . tutView . tut . App . SetInputCapture ( cv . tutView . Input )
cv . tutView . tut . App . SetFocus ( cv . content )
}
func ( cv * ComposeView ) visibilitySelected ( s string , index int ) {
_ , cv . msg . Visibility = cv . visibility . GetCurrentOption ( )
cv . exitVisibility ( )
}
func ( cv * ComposeView ) FocusVisibility ( ) {
cv . tutView . tut . App . SetInputCapture ( cv . visibilityInput )
cv . tutView . tut . App . SetFocus ( cv . visibility )
ev := tcell . NewEventKey ( tcell . KeyDown , 0 , tcell . ModNone )
cv . tutView . tut . App . QueueEvent ( ev )
}
func ( cv * ComposeView ) langInput ( event * tcell . EventKey ) * tcell . EventKey {
if cv . tutView . tut . Config . Input . GlobalDown . Match ( event . Key ( ) , event . Rune ( ) ) {
return tcell . NewEventKey ( tcell . KeyDown , 0 , tcell . ModNone )
}
if cv . tutView . tut . Config . Input . GlobalUp . Match ( event . Key ( ) , event . Rune ( ) ) {
return tcell . NewEventKey ( tcell . KeyUp , 0 , tcell . ModNone )
}
if cv . tutView . tut . Config . Input . GlobalExit . Match ( event . Key ( ) , event . Rune ( ) ) ||
cv . tutView . tut . Config . Input . GlobalBack . Match ( event . Key ( ) , event . Rune ( ) ) {
cv . exitLang ( )
return nil
}
return event
}
func ( cv * ComposeView ) exitLang ( ) {
cv . tutView . tut . App . SetInputCapture ( cv . tutView . Input )
cv . tutView . tut . App . SetFocus ( cv . content )
}
func ( cv * ComposeView ) langSelected ( s string , index int ) {
i , _ := cv . lang . GetCurrentOption ( )
if i >= 0 && i < len ( util . Languages ) {
cv . msg . Language = util . Languages [ i ] . Code
}
cv . exitLang ( )
}
func ( cv * ComposeView ) FocusLang ( ) {
cv . tutView . tut . App . SetInputCapture ( cv . langInput )
cv . tutView . tut . App . SetFocus ( cv . lang )
ev := tcell . NewEventKey ( tcell . KeyDown , 0 , tcell . ModNone )
cv . tutView . tut . App . QueueEvent ( ev )
}
func ( cv * ComposeView ) Post ( ) {
toot := cv . msg
send := mastodon . Toot {
Status : strings . TrimSpace ( toot . Text ) ,
}
if toot . Reply != nil {
send . InReplyToID = toot . Reply . ID
}
if toot . Edit != nil && toot . Edit . InReplyToID != nil {
send . InReplyToID = mastodon . ID ( toot . Edit . InReplyToID . ( string ) )
}
if toot . Sensitive {
send . Sensitive = true
send . SpoilerText = toot . CWText
}
if cv . HasMedia ( ) {
attachments := cv . media . Files
for _ , ap := range attachments {
if ap . Remote {
send . MediaIDs = append ( send . MediaIDs , ap . ID )
continue
}
f , err := os . Open ( ap . Path )
if err != nil {
cv . tutView . ShowError (
fmt . Sprintf ( "Couldn't upload media. Error: %v\n" , err ) ,
)
f . Close ( )
return
}
media := & mastodon . Media {
File : f ,
}
if ap . Description != "" {
media . Description = ap . Description
}
a , err := cv . tutView . tut . Client . Client . UploadMediaFromMedia ( context . Background ( ) , media )
if err != nil {
cv . tutView . ShowError (
fmt . Sprintf ( "Couldn't upload media. Error: %v\n" , err ) ,
)
f . Close ( )
return
}
f . Close ( )
send . MediaIDs = append ( send . MediaIDs , a . ID )
}
}
if cv . tutView . PollView . HasPoll ( ) && ! cv . HasMedia ( ) {
send . Poll = cv . tutView . PollView . GetPoll ( )
}
send . Visibility = cv . msg . Visibility
send . Language = cv . msg . Language
var err error
var newPost * mastodon . Status
if toot . Edit != nil {
newPost , err = cv . tutView . tut . Client . Client . UpdateStatus ( context . Background ( ) , & send , toot . Edit . ID )
if err == nil {
item , itemErr := cv . tutView . GetCurrentItem ( )
if itemErr != nil {
return
}
if item . Type ( ) != api . StatusType {
return
}
s := item . Raw ( ) . ( * mastodon . Status )
* s = * newPost
cv . tutView . RedrawContent ( )
}
} else {
_ , err = cv . tutView . tut . Client . Client . PostStatus ( context . Background ( ) , & send )
}
if err != nil {
cv . tutView . ShowError (
fmt . Sprintf ( "Couldn't post toot. Error: %v\n" , err ) ,
)
return
}
cv . tutView . SetPage ( MainFocus )
}
type MediaList struct {
tutView * TutView
View * tview . Flex
heading * tview . TextView
text * tview . TextView
list * tview . List
Files [ ] UploadFile
scrollSleep * scrollSleep
}
func NewMediaList ( tv * TutView ) * MediaList {
ml := & MediaList {
tutView : tv ,
heading : NewTextView ( tv . tut . Config ) ,
text : NewTextView ( tv . tut . Config ) ,
list : NewList ( tv . tut . Config ) ,
}
ml . scrollSleep = NewScrollSleep ( ml . Next , ml . Prev )
ml . heading . SetText ( fmt . Sprintf ( "Media files: %d" , ml . list . GetItemCount ( ) ) )
ml . heading . SetBorderPadding ( 1 , 1 , 0 , 0 )
ml . View = tview . NewFlex ( ) . SetDirection ( tview . FlexRow ) .
AddItem ( ml . heading , 1 , 0 , false ) .
AddItem ( ml . text , 1 , 0 , false ) .
AddItem ( ml . list , 0 , 1 , false )
return ml
}
type UploadFile struct {
Path string
Description string
Remote bool
ID mastodon . ID
}
func ( m * MediaList ) AddFromEdit ( edit * mastodon . Status ) {
m . Files = nil
m . list . Clear ( )
for i , ma := range edit . MediaAttachments {
m . Files = append ( m . Files , UploadFile {
Description : ma . Description ,
Remote : true ,
ID : ma . ID ,
} )
m . list . AddItem ( fmt . Sprintf ( "From edit: %d" , i + 1 ) , "" , 0 , nil )
}
index := m . list . GetItemCount ( )
if index > 0 {
m . list . SetCurrentItem ( index - 1 )
}
m . Draw ( )
}
func ( m * MediaList ) Reset ( ) {
m . Files = nil
m . list . Clear ( )
m . Draw ( )
}
func ( m * MediaList ) AddFile ( f string ) {
file := UploadFile { Path : f }
m . Files = append ( m . Files , file )
m . list . AddItem ( filepath . Base ( f ) , "" , 0 , nil )
index := m . list . GetItemCount ( )
m . list . SetCurrentItem ( index - 1 )
m . Draw ( )
}
func ( m * MediaList ) Draw ( ) {
topText := "File desc: "
index := m . list . GetCurrentItem ( )
if len ( m . Files ) != 0 && index > len ( m . Files ) - 1 && m . Files [ index ] . Description != "" {
topText += tview . Escape ( m . Files [ index ] . Description )
}
m . text . SetText ( topText )
}
func ( m * MediaList ) SetFocus ( reset bool ) {
if reset {
m . tutView . ComposeView . input . View . SetText ( "" )
return
}
pwd , err := os . Getwd ( )
if err != nil {
home , err := os . UserHomeDir ( )
if err != nil {
pwd = ""
} else {
pwd = home
}
}
if ! strings . HasSuffix ( pwd , "/" ) {
pwd += "/"
}
m . tutView . ComposeView . input . View . SetText ( pwd )
}
func ( m * MediaList ) Prev ( ) {
index := m . list . GetCurrentItem ( )
if index - 1 >= 0 {
m . list . SetCurrentItem ( index - 1 )
}
m . Draw ( )
}
func ( m * MediaList ) Next ( ) {
index := m . list . GetCurrentItem ( )
if index + 1 < m . list . GetItemCount ( ) {
m . list . SetCurrentItem ( index + 1 )
}
m . Draw ( )
}
func ( m * MediaList ) Delete ( ) {
index := m . list . GetCurrentItem ( )
if len ( m . Files ) == 0 || index > len ( m . Files ) - 1 {
return
}
m . list . RemoveItem ( index )
m . list . SetCurrentItem ( index )
m . Files = append ( m . Files [ : index ] , m . Files [ index + 1 : ] ... )
m . Draw ( )
}
func ( m * MediaList ) EditDesc ( ) {
index := m . list . GetCurrentItem ( )
if len ( m . Files ) == 0 || index > len ( m . Files ) {
return
}
file := m . Files [ index ]
if file . Remote {
m . tutView . ShowError (
"Can't edit desc of a file that's already uploaded" ,
)
return
}
var desc string
var err error
if m . tutView . tut . Config . General . UseInternalEditor {
m . tutView . EditorView . Init ( file . Description , 0 , true , func ( input string ) {
m . editDesc ( input , nil )
} )
return
} else {
desc , err = OpenEditor ( m . tutView , file . Description )
m . editDesc ( desc , err )
}
}
func ( m * MediaList ) editDesc ( text string , err error ) {
index := m . list . GetCurrentItem ( )
file := m . Files [ index ]
if err != nil {
m . tutView . ShowError (
fmt . Sprintf ( "Couldn't edit description. Error: %v\n" , err ) ,
)
return
}
file . Description = text
m . Files [ index ] = file
m . Draw ( )
}
type MediaInput struct {
tutView * TutView
View * tview . InputField
text string
autocompleteIndex int
autocompleteList [ ] string
isAutocompleteChange bool
}
func NewMediaInput ( tv * TutView ) * MediaInput {
m := & MediaInput {
tutView : tv ,
View : NewInputField ( tv . tut . Config ) ,
}
m . View . SetChangedFunc ( m . HandleChanges )
return m
}
func ( m * MediaInput ) AddRune ( r rune ) {
newText := m . View . GetText ( ) + string ( r )
m . text = newText
m . View . SetText ( m . text )
m . saveAutocompleteState ( )
}
func ( m * MediaInput ) HandleChanges ( text string ) {
if m . isAutocompleteChange {
m . isAutocompleteChange = false
return
}
m . saveAutocompleteState ( )
}
func ( m * MediaInput ) saveAutocompleteState ( ) {
text := m . View . GetText ( )
m . text = text
m . autocompleteList = util . FindFiles ( text )
m . autocompleteIndex = 0
}
func ( m * MediaInput ) AutocompletePrev ( ) {
if len ( m . autocompleteList ) == 0 {
return
}
index := m . autocompleteIndex - 1
if index < 0 {
index = len ( m . autocompleteList ) - 1
}
m . autocompleteIndex = index
m . showAutocomplete ( )
}
func ( m * MediaInput ) AutocompleteTab ( ) {
if len ( m . autocompleteList ) == 0 {
return
}
same := ""
for i := 0 ; i < len ( m . autocompleteList [ 0 ] ) ; i ++ {
match := true
c := m . autocompleteList [ 0 ] [ i ]
for _ , item := range m . autocompleteList {
if i >= len ( item ) || c != item [ i ] {
match = false
break
}
}
if ! match {
break
}
same += string ( c )
}
if same != m . text {
m . text = same
m . View . SetText ( same )
m . saveAutocompleteState ( )
} else {
m . AutocompleteNext ( )
}
}
func ( m * MediaInput ) AutocompleteNext ( ) {
if len ( m . autocompleteList ) == 0 {
return
}
index := m . autocompleteIndex + 1
if index >= len ( m . autocompleteList ) {
index = 0
}
m . autocompleteIndex = index
m . showAutocomplete ( )
}
func ( m * MediaInput ) CheckDone ( ) {
path := m . View . GetText ( )
if util . IsDir ( path ) {
m . saveAutocompleteState ( )
return
}
m . tutView . ComposeView . media . AddFile ( path )
m . tutView . ComposeView . media . SetFocus ( true )
m . tutView . SetPage ( MediaFocus )
}
func ( m * MediaInput ) showAutocomplete ( ) {
m . isAutocompleteChange = true
m . View . SetText ( m . autocompleteList [ m . autocompleteIndex ] )
if len ( m . autocompleteList ) < 3 {
m . saveAutocompleteState ( )
}
}