package main import ( "bufio" "flag" "fmt" "io" "log" "os" "os/signal" "regexp" "strings" "syscall" "time" qt "github.com/mappu/miqt/qt6" ) // GUIConfig carries all application configurations. type GUIConfig struct { DestinationFolder string PlaylistEnabled bool } // GetDestinationFolder attempts to retrieve (if not yet) and returns the // configured destination folder. func (c *GUIConfig) GetDestinationFolder() string { fallback := DefaultDownloadsFolder() val := settings.Value(*qt.NewQAnyStringView3("destinationFolder"), qt.NewQVariant11(fallback)).ToString() c.DestinationFolder = val return val } // GetPlaylistEnabled attempts to retrieve (if not yet) and returns the // configured value of whether playlist download is enabled. func (c *GUIConfig) GetPlaylistEnabled() bool { fallback := true val := settings.Value(*qt.NewQAnyStringView3("playlistEnabled"), qt.NewQVariant8(fallback)).ToBool() c.PlaylistEnabled = val return val } // SetDestinationFolder sets and persists the configured destination folder. func (c *GUIConfig) SetDestinationFolder(val string) { c.DestinationFolder = val go func() { settings.SetValue(*qt.NewQAnyStringView3("destinationFolder"), qt.NewQVariant11(val)) }() } // SetPlaylistEnabled sets and persists the configured value of whether playlist // download is enabled. func (c *GUIConfig) SetPlaylistEnabled(val bool) { c.PlaylistEnabled = val go func() { settings.SetValue(*qt.NewQAnyStringView3("playlistEnabled"), qt.NewQVariant8(val)) }() } // LoadGUIConfig loads config values from persisted settings if possible, or // loads defaults otherwise. func LoadGUIConfig() *GUIConfig { c := &GUIConfig{} c.GetDestinationFolder() c.GetPlaylistEnabled() return c } var settings *qt.QSettings var guiConfig *GUIConfig func printError(url string, err error) { fmt.Printf( "Downloading %s error:\n%s\n", url, err, ) } // type outputBuffer struct { textEdit *qt.QPlainTextEdit reader *bufio.Reader scanner *bufio.Scanner carriageReturnInAction bool } func newOutputBuffer(textEdit *qt.QPlainTextEdit) *outputBuffer { return &outputBuffer{ textEdit: textEdit, reader: nil, scanner: nil, carriageReturnInAction: false, } } func (b *outputBuffer) attachReader(r io.Reader) { b.reader = bufio.NewReaderSize(r, 64*1024) b.scanner = bufio.NewScanner(b.reader) re := regexp.MustCompile(`^[^\r\n]*(\r\n|\r|\n)`) b.scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { token = re.Find(data) if token != nil { return len(token), token, nil } if !atEOF { return 0, nil, nil } return 0, data, bufio.ErrFinalToken }) } // addLine adds a line that might end in LF, CRLF, CR, or none of the above (in // which case an LF is appended). Aware of CR and scroll position. func (b *outputBuffer) addLine(line string) { scrollBar := b.textEdit.VerticalScrollBar() currentScroll := scrollBar.Value() userScrolledBack := currentScroll != scrollBar.Maximum() cursor := b.textEdit.TextCursor() cursor.MovePosition3(qt.QTextCursor__End, qt.QTextCursor__MoveAnchor, 1) if b.carriageReturnInAction { // Remove last line. cursor.Select(qt.QTextCursor__LineUnderCursor) cursor.RemoveSelectedText() } b.carriageReturnInAction = false if len(line) > 0 { switch lastCh := line[len(line)-1]; lastCh { case '\n': cursor.InsertText(line) case '\r': cursor.InsertText(line[:len(line)-1]) b.carriageReturnInAction = true default: cursor.InsertText(line + "\n") } } else { cursor.InsertText("\n") } if userScrolledBack { scrollBar.SetValue(currentScroll) } else { scrollBar.SetValue(scrollBar.Maximum()) } } func (b *outputBuffer) readLineAndUpdate() (fullLine string, err error) { if !b.scanner.Scan() { err = b.scanner.Err() if err != nil { return "", err } err = io.EOF } fullLine = b.scanner.Text() if len(fullLine) > 0 { b.addLine(fullLine) } return } func init() { flag.Parse() initLanguages() qt.QCoreApplication_SetOrganizationName(appAuthor) qt.QCoreApplication_SetOrganizationDomain(appAuthorDomain) settings = qt.NewQSettings5() guiConfig = LoadGUIConfig() } func initAboutWindow(parent *qt.QWidget) *qt.QDialog { w := qt.NewQDialog(parent) label := qt.NewQLabel3(about) label.SetOpenExternalLinks(true) layout := qt.NewQVBoxLayout2() layout.AddWidget(label.QWidget) w.SetLayout(layout.QLayout) return w } func main() { qt.NewQApplication(os.Args) window := qt.NewQMainWindow2() window.SetWindowTitle(appName) const WindowStateVersion = 0 window.OnCloseEvent(func(super func(event *qt.QCloseEvent), event *qt.QCloseEvent) { settings.SetValue(*qt.NewQAnyStringView3("_geometry"), qt.NewQVariant12(window.SaveGeometry())) settings.SetValue(*qt.NewQAnyStringView3("_windowState"), qt.NewQVariant12(window.SaveState1(WindowStateVersion))) super(event) }) { prevGeometry := settings.Value(*qt.NewQAnyStringView3("_geometry"), qt.NewQVariant()).ToByteArray() if len(prevGeometry) > 0 { window.RestoreGeometry(prevGeometry) } prevWindowState := settings.Value(*qt.NewQAnyStringView3("_windowState"), qt.NewQVariant()).ToByteArray() if len(prevWindowState) > 0 { window.RestoreState2(prevWindowState, WindowStateVersion) } } centralWidget := qt.NewQWidget(window.QWidget) window.SetCentralWidget(centralWidget) menuBar := window.MenuBar() applicationMenu := menuBar.AddMenuWithTitle(tr("Application")) aboutWindow := initAboutWindow(window.QWidget) aboutAction := applicationMenu.AddActionWithText(tr("About")) aboutAction.SetMenuRole(qt.QAction__AboutRole) aboutAction.OnTriggered(func() { aboutWindow.Show() aboutWindow.Raise() }) urlLineEdit := qt.NewQLineEdit(nil) folderLineEdit := qt.NewQLineEdit3(guiConfig.DestinationFolder) folderLineEdit.SetReadOnly(true) folderLineEdit.SetMinimumWidth(250) folderButton := qt.NewQPushButton3(tr("Pick another folder")) folderDialog := qt.NewQFileDialog6(window.QWidget, tr("Destination folder"), guiConfig.DestinationFolder, "") folderDialog.SetFileMode(qt.QFileDialog__Directory) folderButton.OnClicked(func() { if folderDialog.Exec() != int(qt.QDialog__Accepted) { // FIXME blocking call return } destinationFolder := qt.QDir_ToNativeSeparators(folderDialog.SelectedFiles()[0]) folderLineEdit.SetText(destinationFolder) guiConfig.SetDestinationFolder(destinationFolder) }) folderHBoxLayout := qt.NewQHBoxLayout2() folderHBoxLayout.AddWidget3(folderLineEdit.QWidget, 1, 0) folderHBoxLayout.AddWidget3(folderButton.QWidget, 0, 0) playlistCheckBox := qt.NewQCheckBox(nil) playlistCheckBox.SetChecked(guiConfig.PlaylistEnabled) playlistCheckBox.OnToggled(func(checked bool) { guiConfig.SetPlaylistEnabled(checked) }) inputFormLayout := qt.NewQFormLayout(nil) inputFormLayout.SetFieldGrowthPolicy(qt.QFormLayout__AllNonFixedFieldsGrow) inputFormLayout.AddRow3(tr("Video URL"), urlLineEdit.QWidget) inputFormLayout.AddRow4(tr("Destination folder"), folderHBoxLayout.QLayout) inputFormLayout.AddRow3(tr("Download playlists"), playlistCheckBox.QWidget) outputTextEdit := qt.NewQPlainTextEdit2() outputTextEdit.SetReadOnly(true) outputTextEdit.SetMinimumHeight(400) outputTextEdit.SetMinimumWidth(1000) outputTextEdit.SetLineWrapMode(qt.QPlainTextEdit__NoWrap) monospaceFont := qt.NewQFont2("Courier") monospaceFont.SetStyleHint(qt.QFont__Monospace) outputTextEdit.SetFont(monospaceFont) output := newOutputBuffer(outputTextEdit) output.addLine(tr("Awaiting user input")) downloadButton := qt.NewQPushButton3(tr("Download")) downloadButton.OnClicked(func() { url := strings.TrimSpace(urlLineEdit.Text()) if len(url) > 0 { output.addLine(time.Now().Format("15:04:05 ") + tr("Download started")) savedStdout := os.Stdout r, w, _ := os.Pipe() output.attachReader(r) os.Stdout = w go func() { for { _, err := output.readLineAndUpdate() if err != nil { if err == io.EOF { break } log.Fatal(err) } // fmt.Fprint(savedStdout, line) } output.addLine("") }() go func() { if !download(url, guiConfig.PlaylistEnabled, guiConfig.DestinationFolder) { fmt.Println(tr("On network errors, e.g. HTTP 403, please retry a few times.")) } w.Close() os.Stdout = savedStdout }() } }) layout := qt.NewQVBoxLayout2() layout.AddLayout2(inputFormLayout.QLayout, 0) layout.AddWidget3(downloadButton.QWidget, 0, 0) layout.AddWidget3(outputTextEdit.QWidget, 1, 0) centralWidget.SetLayout(layout.QLayout) window.Show() sigs := make(chan os.Signal) signal.Notify(sigs, syscall.SIGSEGV, syscall.SIGABRT) go func() { for { sig := <-sigs log.Print(sig) } }() qt.QApplication_Exec() }