diff --git a/FyneApp.toml b/FyneApp.toml index 3b204ee..1fe1f80 100644 --- a/FyneApp.toml +++ b/FyneApp.toml @@ -2,7 +2,7 @@ Icon = "icon.png" Name = "GUI for FFmpeg" ID = "net.kor-elf.projects.gui-for-ffmpeg" - Version = "0.8.0" + Version = "0.9.0" Build = 4 [Migrations] diff --git a/convertor/repository.go b/convertor/repository.go index e31f64a..f751607 100644 --- a/convertor/repository.go +++ b/convertor/repository.go @@ -9,6 +9,8 @@ type RepositoryContract interface { SavePathFfmpeg(code string) (setting.Setting, error) GetPathFfprobe() (string, error) SavePathFfprobe(code string) (setting.Setting, error) + GetPathFfplay() (string, error) + SavePathFfplay(code string) (setting.Setting, error) } type Repository struct { @@ -34,3 +36,11 @@ func (r Repository) GetPathFfprobe() (string, error) { func (r Repository) SavePathFfprobe(path string) (setting.Setting, error) { return r.settingRepository.CreateOrUpdate("ffprobe", path) } + +func (r Repository) GetPathFfplay() (string, error) { + return r.settingRepository.GetValue("ffplay") +} + +func (r Repository) SavePathFfplay(path string) (setting.Setting, error) { + return r.settingRepository.CreateOrUpdate("ffplay", path) +} diff --git a/convertor/view.go b/convertor/view.go index 35056fb..58e6a3f 100644 --- a/convertor/view.go +++ b/convertor/view.go @@ -18,7 +18,8 @@ type ViewContract interface { SelectFFPath( ffmpegPath string, ffprobePath string, - save func(ffmpegPath string, ffprobePath string) error, + ffplayPath string, + save func(ffmpegPath string, ffprobePath string, ffplayPath string) error, cancel func(), donwloadFFmpeg func(progressBar *widget.ProgressBar, progressMessage *canvas.Text) error, ) diff --git a/convertor/view/conversion.go b/convertor/view/conversion.go index 5d95e18..947a37c 100644 --- a/convertor/view/conversion.go +++ b/convertor/view/conversion.go @@ -24,30 +24,30 @@ type ConversionContract interface { } type Conversion struct { - app kernel.AppContract - form *form - conversionMessage *canvas.Text - fileForConversion *fileForConversion - directoryForSaving *directoryForSaving - overwriteOutputFiles *overwriteOutputFiles - selectEncoder *selectEncoder - runConvert func(setting HandleConvertSetting) + app kernel.AppContract + form *form + conversionMessage *canvas.Text + fileForConversion *fileForConversion + directoryForSaving *directoryForSaving + overwriteOutputFiles *overwriteOutputFiles + selectEncoder *selectEncoder + runConvert func(setting HandleConvertSetting) + itemsToConvertService kernel.ItemsToConvertContract } type HandleConvertSetting struct { - FileInput kernel.File DirectoryForSave string OverwriteOutputFiles bool Format string Encoder encoder2.EncoderContract } -func NewConversion(app kernel.AppContract, formats encoder.ConvertorFormatsContract, runConvert func(setting HandleConvertSetting), settingDirectoryForSaving setting.DirectoryForSavingContract) *Conversion { +func NewConversion(app kernel.AppContract, formats encoder.ConvertorFormatsContract, runConvert func(setting HandleConvertSetting), settingDirectoryForSaving setting.DirectoryForSavingContract, itemsToConvertService kernel.ItemsToConvertContract) *Conversion { conversionMessage := canvas.NewText("", color.RGBA{R: 255, G: 0, B: 0, A: 255}) conversionMessage.TextSize = 16 conversionMessage.TextStyle = fyne.TextStyle{Bold: true} - fileForConversion := newFileForConversion(app) + fileForConversion := newFileForConversion(app, itemsToConvertService) directoryForSaving := newDirectoryForSaving(app, settingDirectoryForSaving) overwriteOutputFiles := newOverwriteOutputFiles(app) selectEncoder := newSelectEncoder(app, formats) @@ -85,14 +85,15 @@ func NewConversion(app kernel.AppContract, formats encoder.ConvertorFormatsContr form := newForm(app, items) return &Conversion{ - app: app, - form: form, - conversionMessage: conversionMessage, - fileForConversion: fileForConversion, - directoryForSaving: directoryForSaving, - overwriteOutputFiles: overwriteOutputFiles, - selectEncoder: selectEncoder, - runConvert: runConvert, + app: app, + form: form, + conversionMessage: conversionMessage, + fileForConversion: fileForConversion, + directoryForSaving: directoryForSaving, + overwriteOutputFiles: overwriteOutputFiles, + selectEncoder: selectEncoder, + runConvert: runConvert, + itemsToConvertService: itemsToConvertService, } } @@ -121,20 +122,32 @@ func (c Conversion) changeEncoder(encoder encoder2.EncoderContract) { } func (c Conversion) AfterViewContent() { - c.form.form.Disable() + if len(c.itemsToConvertService.GetItems()) == 0 { + c.form.form.Disable() + } } func (c Conversion) selectFileForConversion(err error) { c.conversionMessage.Text = "" - if err != nil { - c.form.form.Disable() - return + if len(c.itemsToConvertService.GetItems()) == 0 { + if err != nil { + c.form.form.Disable() + return + } } c.form.form.Enable() } func (c Conversion) submit() { + if len(c.itemsToConvertService.GetItems()) == 0 { + showConversionMessage(c.conversionMessage, errors.New(c.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "errorNoFilesAddedForConversion", + }))) + c.enableFormConversion() + return + } + if len(c.directoryForSaving.path) == 0 { showConversionMessage(c.conversionMessage, errors.New(c.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "errorSelectedFolderSave", @@ -160,18 +173,17 @@ func (c Conversion) submit() { c.directoryForSaving.button.Disable() c.form.form.Disable() - setting := HandleConvertSetting{ - FileInput: *c.fileForConversion.file, + c.runConvert(HandleConvertSetting{ DirectoryForSave: c.directoryForSaving.path, OverwriteOutputFiles: c.overwriteOutputFiles.IsChecked(), Format: c.selectEncoder.SelectFormat.Selected, Encoder: c.selectEncoder.Encoder, - } - c.runConvert(setting) + }) c.enableFormConversion() - c.fileForConversion.message.Text = "" - c.form.form.Disable() + if len(c.itemsToConvertService.GetItems()) == 0 { + c.form.form.Disable() + } } func (c Conversion) enableFormConversion() { @@ -188,44 +200,49 @@ type fileForConversion struct { changeCallbacks map[int]func(err error) } -func newFileForConversion(app kernel.AppContract) *fileForConversion { +func newFileForConversion(app kernel.AppContract, itemsToConvertService kernel.ItemsToConvertContract) *fileForConversion { + message := canvas.NewText("", color.RGBA{R: 255, G: 0, B: 0, A: 255}) fileForConversion := &fileForConversion{ - file: &kernel.File{}, + message: message, + changeCallbacks: map[int]func(err error){}, } buttonTitle := app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "choose", - }) + "\n\r\n\r" + app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + }) + "\n" + app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "or", - }) + "\n\r\n\r" + app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "dragAndDrop1File", + }) + "\n" + app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "dragAndDropFiles", }) - fileForConversion.message = canvas.NewText("", color.RGBA{R: 255, G: 0, B: 0, A: 255}) - fileForConversion.message.TextSize = 16 - fileForConversion.message.TextStyle = fyne.TextStyle{Bold: true} - var locationURI fyne.ListableURI fileForConversion.button = widget.NewButton(buttonTitle, func() { app.GetWindow().NewFileOpen(func(r fyne.URIReadCloser, err error) { + fyne.Do(func() { + fileForConversion.message.Text = "" + fileForConversion.message.Refresh() + }) + if err != nil { - fileForConversion.message.Text = err.Error() - setStringErrorStyle(fileForConversion.message) + fyne.Do(func() { + fileForConversion.message.Text = err.Error() + fileForConversion.message.Refresh() + }) fileForConversion.eventSelectFile(err) return } if r == nil { return } + app.GetWindow().GetLayout().GetRightTabs().SelectAddedFilesTab() - fileForConversion.file.Path = r.URI().Path() - fileForConversion.file.Name = r.URI().Name() - fileForConversion.file.Ext = r.URI().Extension() - - fileForConversion.message.Text = r.URI().Path() - setStringSuccessStyle(fileForConversion.message) + itemsToConvertService.Add(&kernel.File{ + Path: r.URI().Path(), + Name: r.URI().Name(), + Ext: r.URI().Extension(), + }) fileForConversion.eventSelectFile(nil) @@ -239,43 +256,42 @@ func newFileForConversion(app kernel.AppContract) *fileForConversion { return } - if len(uris) > 1 { + isError := false + for _, uri := range uris { + info, err := os.Stat(uri.Path()) + if err != nil { + isError = true + continue + } + if info.IsDir() { + isError = true + continue + } + + itemsToConvertService.Add(&kernel.File{ + Path: uri.Path(), + Name: uri.Name(), + Ext: uri.Extension(), + }) + + fileForConversion.eventSelectFile(nil) + + listableURI := storage.NewFileURI(filepath.Dir(uri.Path())) + locationURI, _ = storage.ListerForURI(listableURI) + } + app.GetWindow().GetLayout().GetRightTabs().SelectAddedFilesTab() + if isError { fileForConversion.message.Text = app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "errorDragAndDrop1File", + MessageID: "errorDragAndDropFile", }) setStringErrorStyle(fileForConversion.message) fileForConversion.eventSelectFile(errors.New(fileForConversion.message.Text)) - return - } - - uri := uris[0] - info, err := os.Stat(uri.Path()) - if err != nil { - fileForConversion.message.Text = err.Error() - setStringErrorStyle(fileForConversion.message) - fileForConversion.eventSelectFile(err) - return - } - if info.IsDir() { - fileForConversion.message.Text = app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "errorIsFolder", + } else { + fyne.Do(func() { + fileForConversion.message.Text = "" + fileForConversion.message.Refresh() }) - setStringErrorStyle(fileForConversion.message) - fileForConversion.eventSelectFile(errors.New(fileForConversion.message.Text)) - return } - - fileForConversion.file.Path = uri.Path() - fileForConversion.file.Name = uri.Name() - fileForConversion.file.Ext = uri.Extension() - - fileForConversion.message.Text = uri.Path() - setStringSuccessStyle(fileForConversion.message) - - fileForConversion.eventSelectFile(nil) - - listableURI := storage.NewFileURI(filepath.Dir(uri.Path())) - locationURI, _ = storage.ListerForURI(listableURI) }) return fileForConversion diff --git a/convertor/view_setting.go b/convertor/view_setting.go index b52315c..ca5e3f4 100644 --- a/convertor/view_setting.go +++ b/convertor/view_setting.go @@ -15,7 +15,8 @@ import ( func (v View) SelectFFPath( currentPathFfmpeg string, currentPathFfprobe string, - save func(ffmpegPath string, ffprobePath string) error, + currentPathFfplay string, + save func(ffmpegPath string, ffprobePath string, ffplayPath string) error, cancel func(), donwloadFFmpeg func(progressBar *widget.ProgressBar, progressMessage *canvas.Text) error, ) { @@ -25,6 +26,7 @@ func (v View) SelectFFPath( ffmpegPath, buttonFFmpeg, buttonFFmpegMessage := v.getButtonSelectFile(currentPathFfmpeg) ffprobePath, buttonFFprobe, buttonFFprobeMessage := v.getButtonSelectFile(currentPathFfprobe) + ffplayPath, buttonFFplay, buttonFFplayMessage := v.getButtonSelectFile(currentPathFfplay) link := widget.NewHyperlink("https://ffmpeg.org/download.html", &url.URL{ Scheme: "https", @@ -58,6 +60,15 @@ func (v View) SelectFFPath( { Widget: container.NewHScroll(buttonFFprobeMessage), }, + { + Text: v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "pathToFfplay", + }), + Widget: buttonFFplay, + }, + { + Widget: container.NewHScroll(buttonFFplayMessage), + }, { Widget: errorMessage, }, @@ -66,7 +77,7 @@ func (v View) SelectFFPath( MessageID: "save", }), OnSubmit: func() { - err := save(*ffmpegPath, *ffprobePath) + err := save(*ffmpegPath, *ffprobePath, *ffplayPath) if err != nil { errorMessage.Text = err.Error() } diff --git a/error/view.go b/error/view.go index 4845ab0..2d68856 100644 --- a/error/view.go +++ b/error/view.go @@ -1,11 +1,14 @@ package error import ( + "errors" "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/lang" "fyne.io/fyne/v2/widget" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/localizer" "github.com/nicksnyder/go-i18n/v2/i18n" + "go.etcd.io/bbolt" ) type ViewContract interface { @@ -13,24 +16,38 @@ type ViewContract interface { } type View struct { - app kernel.AppContract + app kernel.AppContract + isSetLanguage bool } func NewView(app kernel.AppContract) *View { return &View{ - app: app, + app: app, + isSetLanguage: true, } } func (v View) PanicError(err error) { + if v.isSetLanguage { + v.isSetLanguage = false + _ = v.app.GetLocalizerService().SetCurrentLanguageByCode(lang.SystemLocale().LanguageString()) + } + messageHead := v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "error", }) + messagetText := err.Error() + if errors.Is(err, bbolt.ErrTimeout) { + messagetText = v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "errorDatabaseTimeout", + }) + } + v.app.GetWindow().SetContent(container.NewBorder( container.NewVBox( widget.NewLabel(messageHead), - widget.NewLabel(err.Error()), + widget.NewLabel(messagetText), ), nil, nil, @@ -42,6 +59,11 @@ func (v View) PanicError(err error) { } func (v View) PanicErrorWriteDirectoryData() { + if v.isSetLanguage { + v.isSetLanguage = false + _ = v.app.GetLocalizerService().SetCurrentLanguageByCode(lang.SystemLocale().LanguageString()) + } + message := v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "errorDatabase", }) diff --git a/handler/convertor.go b/handler/convertor.go index 4f0b2ab..a0cd29b 100644 --- a/handler/convertor.go +++ b/handler/convertor.go @@ -16,6 +16,7 @@ type ConvertorHandlerContract interface { FfPathSelection() GetFfmpegVersion() (string, error) GetFfprobeVersion() (string, error) + GetFfplayVersion() (string, error) } type ConvertorHandler struct { @@ -24,6 +25,7 @@ type ConvertorHandler struct { errorView error2.ViewContract convertorRepository convertor.RepositoryContract settingDirectoryForSaving setting.DirectoryForSavingContract + itemsToConvertService kernel.ItemsToConvertContract } func NewConvertorHandler( @@ -32,6 +34,7 @@ func NewConvertorHandler( errorView error2.ViewContract, convertorRepository convertor.RepositoryContract, settingDirectoryForSaving setting.DirectoryForSavingContract, + itemsToConvertService kernel.ItemsToConvertContract, ) *ConvertorHandler { return &ConvertorHandler{ app: app, @@ -39,6 +42,7 @@ func NewConvertorHandler( errorView: errorView, convertorRepository: convertorRepository, settingDirectoryForSaving: settingDirectoryForSaving, + itemsToConvertService: itemsToConvertService, } } @@ -49,17 +53,18 @@ func (h ConvertorHandler) MainConvertor() { h.errorView.PanicError(err) return } - conversion := view.NewConversion(h.app, formats, h.runConvert, h.settingDirectoryForSaving) + conversion := view.NewConversion(h.app, formats, h.runConvert, h.settingDirectoryForSaving, h.itemsToConvertService) h.convertorView.Main(conversion) return } - h.convertorView.SelectFFPath("", "", h.saveSettingFFPath, nil, h.downloadFFmpeg) + h.convertorView.SelectFFPath("", "", "", h.saveSettingFFPath, nil, h.downloadFFmpeg) } func (h ConvertorHandler) FfPathSelection() { ffmpeg, _ := h.convertorRepository.GetPathFfmpeg() ffprobe, _ := h.convertorRepository.GetPathFfprobe() - h.convertorView.SelectFFPath(ffmpeg, ffprobe, h.saveSettingFFPath, h.MainConvertor, h.downloadFFmpeg) + ffplay, _ := h.convertorRepository.GetPathFfplay() + h.convertorView.SelectFFPath(ffmpeg, ffprobe, ffplay, h.saveSettingFFPath, h.MainConvertor, h.downloadFFmpeg) } func (h ConvertorHandler) GetFfmpegVersion() (string, error) { @@ -70,17 +75,31 @@ func (h ConvertorHandler) GetFfprobeVersion() (string, error) { return h.app.GetConvertorService().GetFFprobeVersion() } +func (h ConvertorHandler) GetFfplayVersion() (string, error) { + return h.app.GetConvertorService().GetFFplayVersion() +} + func (h ConvertorHandler) runConvert(setting view.HandleConvertSetting) { - h.app.GetQueue().Add(&kernel.ConvertSetting{ - VideoFileInput: setting.FileInput, - VideoFileOut: kernel.File{ - Path: setting.DirectoryForSave + helper.PathSeparator() + setting.FileInput.Name + "." + setting.Format, - Name: setting.FileInput.Name, - Ext: "." + setting.Format, - }, - OverwriteOutputFiles: setting.OverwriteOutputFiles, - Encoder: setting.Encoder, - }) + h.app.GetWindow().GetLayout().GetRightTabs().SelectFileQueueTab() + + for _, item := range h.itemsToConvertService.GetItems() { + file := item.GetFile() + if file == nil { + continue + } + + h.app.GetQueue().Add(&kernel.ConvertSetting{ + VideoFileInput: *file, + VideoFileOut: kernel.File{ + Path: setting.DirectoryForSave + helper.PathSeparator() + file.Name + "." + setting.Format, + Name: file.Name, + Ext: "." + setting.Format, + }, + OverwriteOutputFiles: setting.OverwriteOutputFiles, + Encoder: setting.Encoder, + }) + } + h.itemsToConvertService.AfterAddingQueue() } func (h ConvertorHandler) checkingFFPathUtilities() bool { @@ -98,15 +117,21 @@ func (h ConvertorHandler) checkingFFPathUtilities() bool { if ffprobeChecking == false { continue } + + ffplayChecking, _ := h.app.GetConvertorService().ChangeFFplayPath(item.FFplay) + if ffplayChecking == false { + continue + } _, _ = h.convertorRepository.SavePathFfmpeg(item.FFmpeg) _, _ = h.convertorRepository.SavePathFfprobe(item.FFprobe) + _, _ = h.convertorRepository.SavePathFfplay(item.FFplay) return true } return false } -func (h ConvertorHandler) saveSettingFFPath(ffmpegPath string, ffprobePath string) error { +func (h ConvertorHandler) saveSettingFFPath(ffmpegPath string, ffprobePath string, ffplayPath string) error { ffmpegChecking, _ := h.app.GetConvertorService().ChangeFFmpegPath(ffmpegPath) if ffmpegChecking == false { errorText := h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ @@ -123,8 +148,17 @@ func (h ConvertorHandler) saveSettingFFPath(ffmpegPath string, ffprobePath strin return errors.New(errorText) } + ffplayChecking, _ := h.app.GetConvertorService().ChangeFFplayPath(ffplayPath) + if ffplayChecking == false { + errorText := h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "errorFFplay", + }) + return errors.New(errorText) + } + _, _ = h.convertorRepository.SavePathFfmpeg(ffmpegPath) _, _ = h.convertorRepository.SavePathFfprobe(ffprobePath) + _, _ = h.convertorRepository.SavePathFfplay(ffplayPath) h.MainConvertor() @@ -142,5 +176,10 @@ func (h ConvertorHandler) checkingFFPath() bool { return false } + _, err = h.app.GetConvertorService().GetFFplayVersion() + if err != nil { + return false + } + return true } diff --git a/handler/convertor_anyos.go b/handler/convertor_anyos.go index 5143398..76daa45 100644 --- a/handler/convertor_anyos.go +++ b/handler/convertor_anyos.go @@ -10,7 +10,7 @@ import ( ) func getPathsToFF() []kernel.FFPathUtilities { - return []kernel.FFPathUtilities{{"ffmpeg/bin/ffmpeg", "ffmpeg/bin/ffprobe"}, {"ffmpeg", "ffprobe"}} + return []kernel.FFPathUtilities{{FFmpeg: "ffmpeg/bin/ffmpeg", FFprobe: "ffmpeg/bin/ffprobe", FFplay: "ffmpeg/bin/ffplay"}, {FFmpeg: "ffmpeg", FFprobe: "ffprobe", FFplay: "ffplay"}} } func (h ConvertorHandler) downloadFFmpeg(progressBar *widget.ProgressBar, progressMessage *canvas.Text) (err error) { diff --git a/handler/convertor_linux.go b/handler/convertor_linux.go index e0995f2..c1a032a 100644 --- a/handler/convertor_linux.go +++ b/handler/convertor_linux.go @@ -19,7 +19,7 @@ import ( ) func getPathsToFF() []kernel.FFPathUtilities { - return []kernel.FFPathUtilities{{"ffmpeg/bin/ffmpeg", "ffmpeg/bin/ffprobe"}, {"ffmpeg", "ffprobe"}} + return []kernel.FFPathUtilities{{FFmpeg: "ffmpeg/bin/ffmpeg", FFprobe: "ffmpeg/bin/ffprobe", FFplay: "ffmpeg/bin/ffplay"}, {FFmpeg: "ffmpeg", FFprobe: "ffprobe", FFplay: "ffplay"}} } func (h ConvertorHandler) downloadFFmpeg(progressBar *widget.ProgressBar, progressMessage *canvas.Text) (err error) { @@ -60,7 +60,11 @@ func (h ConvertorHandler) downloadFFmpeg(progressBar *widget.ProgressBar, progre progressMessage.Refresh() }) - err = h.saveSettingFFPath("ffmpeg/ffmpeg-master-latest-linux64-gpl/bin/ffmpeg", "ffmpeg/ffmpeg-master-latest-linux64-gpl/bin/ffprobe") + err = h.saveSettingFFPath( + "ffmpeg/ffmpeg-master-latest-linux64-gpl/bin/ffmpeg", + "ffmpeg/ffmpeg-master-latest-linux64-gpl/bin/ffprobe", + "ffmpeg/ffmpeg-master-latest-linux64-gpl/bin/ffplay", + ) if err != nil { return err } @@ -217,6 +221,12 @@ func unTarXz(fileTar string, directory string, progressBar *widget.ProgressBar) return err } + ffplayPath := filepath.Join(directory, "ffmpeg-master-latest-linux64-gpl", "bin", "ffplay") + err = os.Chmod(ffplayPath, 0755) + if err != nil { + return err + } + return nil } diff --git a/handler/convertor_windows.go b/handler/convertor_windows.go index 3a31e5e..fb08d87 100644 --- a/handler/convertor_windows.go +++ b/handler/convertor_windows.go @@ -19,7 +19,7 @@ import ( ) func getPathsToFF() []kernel.FFPathUtilities { - return []kernel.FFPathUtilities{{"ffmpeg\\bin\\ffmpeg.exe", "ffmpeg\\bin\\ffprobe.exe"}} + return []kernel.FFPathUtilities{{FFmpeg: "ffmpeg\\bin\\ffmpeg.exe", FFprobe: "ffmpeg\\bin\\ffprobe.exe", FFplay: "ffmpeg\\bin\\ffplay.exe"}} } func (h ConvertorHandler) downloadFFmpeg(progressBar *widget.ProgressBar, progressMessage *canvas.Text) (err error) { @@ -59,7 +59,11 @@ func (h ConvertorHandler) downloadFFmpeg(progressBar *widget.ProgressBar, progre fyne.Do(func() { progressMessage.Refresh() }) - err = h.saveSettingFFPath("ffmpeg/ffmpeg-master-latest-win64-gpl/bin/ffmpeg.exe", "ffmpeg/ffmpeg-master-latest-win64-gpl/bin/ffprobe.exe") + err = h.saveSettingFFPath( + "ffmpeg/ffmpeg-master-latest-win64-gpl/bin/ffmpeg.exe", + "ffmpeg/ffmpeg-master-latest-win64-gpl/bin/ffprobe.exe", + "ffmpeg/ffmpeg-master-latest-win64-gpl/bin/ffplay.exe", + ) if err != nil { return err } diff --git a/handler/main.go b/handler/main.go index e2e6398..0e97c6f 100644 --- a/handler/main.go +++ b/handler/main.go @@ -1,6 +1,7 @@ package handler import ( + "fyne.io/fyne/v2/lang" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/localizer" ) @@ -29,8 +30,11 @@ func NewMainHandler( func (h MainHandler) Start() { language, err := h.localizerRepository.GetCode() if err != nil { - h.menuHandler.LanguageSelection() - return + err = h.app.GetLocalizerService().SetCurrentLanguageByCode(lang.SystemLocale().LanguageString()) + if err != nil { + h.menuHandler.LanguageSelection() + return + } } _ = h.app.GetLocalizerService().SetCurrentLanguageByCode(language) diff --git a/handler/menu.go b/handler/menu.go index 79668c9..fa32d8b 100644 --- a/handler/menu.go +++ b/handler/menu.go @@ -5,6 +5,7 @@ import ( "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/localizer" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/menu" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/theme" "github.com/nicksnyder/go-i18n/v2/i18n" ) @@ -17,23 +18,29 @@ type MenuHandler struct { app kernel.AppContract convertorHandler ConvertorHandlerContract menuView menu.ViewContract + menuViewSetting menu.ViewSettingContract localizerView localizer.ViewContract localizerRepository localizer.RepositoryContract + themeService theme.ThemeContract } func NewMenuHandler( app kernel.AppContract, convertorHandler ConvertorHandlerContract, menuView menu.ViewContract, + menuViewSetting menu.ViewSettingContract, localizerView localizer.ViewContract, localizerRepository localizer.RepositoryContract, + themeService theme.ThemeContract, ) *MenuHandler { return &MenuHandler{ app: app, convertorHandler: convertorHandler, menuView: menuView, + menuViewSetting: menuViewSetting, localizerView: localizerView, localizerRepository: localizerRepository, + themeService: themeService, } } @@ -53,11 +60,11 @@ func (h MenuHandler) getMenuSettings() *fyne.Menu { quit.Label = text }) - languageSelection := fyne.NewMenuItem(h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "changeLanguage", - }), h.LanguageSelection) - h.app.GetLocalizerService().AddChangeCallback("changeLanguage", func(text string) { - languageSelection.Label = text + settingsSelection := fyne.NewMenuItem(h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "settings", + }), h.settingsSelection) + h.app.GetLocalizerService().AddChangeCallback("settings", func(text string) { + settingsSelection.Label = text }) ffPathSelection := fyne.NewMenuItem(h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ @@ -69,7 +76,7 @@ func (h MenuHandler) getMenuSettings() *fyne.Menu { settings := fyne.NewMenu(h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "settings", - }), languageSelection, ffPathSelection, quit) + }), settingsSelection, ffPathSelection, quit) h.app.GetLocalizerService().AddChangeCallback("settings", func(text string) { settings.Label = text settings.Refresh() @@ -88,14 +95,21 @@ func (h MenuHandler) getMenuHelp() *fyne.Menu { gratitude := fyne.NewMenuItem(h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "gratitude", - }), h.openGratitude) + }), h.menuView.Gratitude) h.app.GetLocalizerService().AddChangeCallback("gratitude", func(text string) { gratitude.Label = text }) + helpFFplay := fyne.NewMenuItem(h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "helpFFplay", + }), h.menuView.HelpFFplay) + h.app.GetLocalizerService().AddChangeCallback("helpFFplay", func(text string) { + helpFFplay.Label = text + }) + help := fyne.NewMenu(h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "help", - }), about, gratitude) + }), helpFFplay, about, gratitude) h.app.GetLocalizerService().AddChangeCallback("help", func(text string) { help.Label = text help.Refresh() @@ -117,12 +131,14 @@ func (h MenuHandler) openAbout() { MessageID: "errorFFprobeVersion", }) } + ffplay, err := h.convertorHandler.GetFfplayVersion() + if err != nil { + ffplay = h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "errorFFplayVersion", + }) + } - h.menuView.About(ffmpeg, ffprobe) -} - -func (h MenuHandler) openGratitude() { - h.menuView.Gratitude() + h.menuView.About(ffmpeg, ffprobe, ffplay) } func (h MenuHandler) LanguageSelection() { @@ -131,3 +147,28 @@ func (h MenuHandler) LanguageSelection() { h.convertorHandler.MainConvertor() }) } + +func (h MenuHandler) settingsSelection() { + save := func(setting *menu.SettingForm) error { + err := h.app.GetLocalizerService().SetCurrentLanguage(setting.Language) + if err != nil { + return err + } + _, err = h.localizerRepository.Save(setting.Language.Code) + if err != nil { + return err + } + + err = h.themeService.SetCurrentTheme(setting.ThemeInfo) + if err != nil { + return err + } + + h.convertorHandler.MainConvertor() + return nil + } + cancel := func() { + h.convertorHandler.MainConvertor() + } + h.menuViewSetting.Main(save, cancel) +} diff --git a/images/screenshot-gui-for-ffmpeg.png b/images/screenshot-gui-for-ffmpeg.png index fba50e6..0fc23fd 100644 Binary files a/images/screenshot-gui-for-ffmpeg.png and b/images/screenshot-gui-for-ffmpeg.png differ diff --git a/kernel/app.go b/kernel/app.go index e1ec4f9..3babb14 100644 --- a/kernel/app.go +++ b/kernel/app.go @@ -12,6 +12,7 @@ type AppContract interface { GetQueue() QueueListContract GetLocalizerService() LocalizerContract GetConvertorService() ConvertorContract + GetFFplayService() FFplayContract AfterClosing() RunConvertor() } @@ -21,27 +22,36 @@ type App struct { Window WindowContract Queue QueueListContract - localizerService LocalizerContract - convertorService ConvertorContract + localizerService LocalizerContract + convertorService ConvertorContract + blockProgressbarService BlockProgressbarContract + ffplayService FFplayContract } func NewApp( metadata *fyne.AppMetadata, localizerService LocalizerContract, queue QueueListContract, - queueLayoutObject QueueLayoutObjectContract, + ffplayService FFplayContract, convertorService ConvertorContract, ) *App { app.SetMetadata(*metadata) a := app.New() + statusesText := GetBlockProgressbarStatusesText(localizerService) + blockProgressbarService := NewBlockProgressbar(statusesText, ffplayService) + rightTabsService := NewRightTabs(localizerService) + queueLayoutObject := NewQueueLayoutObject(queue, localizerService, ffplayService, rightTabsService, blockProgressbarService.GetContainer()) + return &App{ AppFyne: a, - Window: newWindow(a.NewWindow("GUI for FFmpeg"), NewLayout(queueLayoutObject, localizerService)), + Window: newWindow(a.NewWindow("GUI for FFmpeg"), NewLayout(queueLayoutObject, localizerService, rightTabsService)), Queue: queue, - localizerService: localizerService, - convertorService: convertorService, + localizerService: localizerService, + convertorService: convertorService, + blockProgressbarService: blockProgressbarService, + ffplayService: ffplayService, } } @@ -65,6 +75,10 @@ func (a App) GetConvertorService() ConvertorContract { return a.convertorService } +func (a App) GetFFplayService() FFplayContract { + return a.ffplayService +} + func (a App) AfterClosing() { for _, cmd := range a.convertorService.GetRunningProcesses() { _ = cmd.Process.Kill() @@ -81,22 +95,33 @@ func (a App) RunConvertor() { } queue.Status = StatusType(InProgress) a.Window.GetLayout().ChangeQueueStatus(queueId, queue) + if a.blockProgressbarService.GetContainer().Hidden { + a.blockProgressbarService.GetContainer().Show() + } totalDuration, err := a.convertorService.GetTotalDuration(&queue.Setting.VideoFileInput) if err != nil { totalDuration = 0 } - progress := a.Window.GetLayout().NewProgressbar(queueId, totalDuration) + + progress := a.blockProgressbarService.GetProgressbar( + totalDuration, + queue.Setting.VideoFileInput.Path, + a.localizerService, + ) err = a.convertorService.RunConvert(*queue.Setting, progress) if err != nil { queue.Status = StatusType(Error) queue.Error = err a.Window.GetLayout().ChangeQueueStatus(queueId, queue) + a.blockProgressbarService.ProcessEndedWithError(err.Error()) + continue } queue.Status = StatusType(Completed) a.Window.GetLayout().ChangeQueueStatus(queueId, queue) + a.blockProgressbarService.ProcessEndedWithSuccess(queue.Setting.VideoFileOut.Path) } }() } diff --git a/kernel/convertor.go b/kernel/convertor.go index ac765b5..179cff3 100644 --- a/kernel/convertor.go +++ b/kernel/convertor.go @@ -32,8 +32,10 @@ type ConvertorContract interface { GetTotalDuration(file *File) (float64, error) GetFFmpegVesrion() (string, error) GetFFprobeVersion() (string, error) + GetFFplayVersion() (string, error) ChangeFFmpegPath(path string) (bool, error) ChangeFFprobePath(path string) (bool, error) + ChangeFFplayPath(path string) (bool, error) GetRunningProcesses() map[int]*exec.Cmd GetSupportFormats() (encoder.ConvertorFormatsContract, error) } @@ -46,6 +48,7 @@ type ProgressContract interface { type FFPathUtilities struct { FFmpeg string FFprobe string + FFplay string } type runningProcesses struct { @@ -177,6 +180,17 @@ func (s Convertor) GetFFprobeVersion() (string, error) { return text[0], nil } +func (s Convertor) GetFFplayVersion() (string, error) { + cmd := exec.Command(s.ffPathUtilities.FFplay, "-version") + helper.PrepareBackgroundCommand(cmd) + out, err := cmd.CombinedOutput() + if err != nil { + return "", err + } + text := regexp.MustCompile("\r?\n").Split(strings.TrimSpace(string(out)), -1) + return text[0], nil +} + func (s Convertor) ChangeFFmpegPath(path string) (bool, error) { cmd := exec.Command(path, "-version") helper.PrepareBackgroundCommand(cmd) @@ -205,6 +219,20 @@ func (s Convertor) ChangeFFprobePath(path string) (bool, error) { return true, nil } +func (s Convertor) ChangeFFplayPath(path string) (bool, error) { + cmd := exec.Command(path, "-version") + helper.PrepareBackgroundCommand(cmd) + out, err := cmd.CombinedOutput() + if err != nil { + return false, err + } + if strings.Contains(strings.TrimSpace(string(out)), "ffplay") == false { + return false, nil + } + s.ffPathUtilities.FFplay = path + return true, nil +} + func (s Convertor) GetSupportFormats() (encoder.ConvertorFormatsContract, error) { formats := encoder.NewConvertorFormats() cmd := exec.Command(s.ffPathUtilities.FFmpeg, "-encoders") diff --git a/kernel/ffplay.go b/kernel/ffplay.go new file mode 100644 index 0000000..0b5538f --- /dev/null +++ b/kernel/ffplay.go @@ -0,0 +1,28 @@ +package kernel + +import ( + "os/exec" +) + +type FFplay struct { + ffPathUtilities *FFPathUtilities +} + +type FFplaySetting struct { + PathToFile string +} + +type FFplayContract interface { + Run(setting FFplaySetting) error +} + +func NewFFplay(ffPathUtilities *FFPathUtilities) *FFplay { + return &FFplay{ffPathUtilities: ffPathUtilities} +} + +func (ffplay FFplay) Run(setting FFplaySetting) error { + args := []string{setting.PathToFile} + cmd := exec.Command(ffplay.ffPathUtilities.FFplay, args...) + + return cmd.Start() +} diff --git a/kernel/items_to_convert.go b/kernel/items_to_convert.go new file mode 100644 index 0000000..0adbc3d --- /dev/null +++ b/kernel/items_to_convert.go @@ -0,0 +1,151 @@ +package kernel + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" + "github.com/nicksnyder/go-i18n/v2/i18n" +) + +type ItemsToConvertContract interface { + Add(file *File) + GetItems() map[int]ItemToConvertContract + AfterAddingQueue() +} + +type ItemsToConvert struct { + nextId int + items map[int]ItemToConvertContract + itemsContainer *fyne.Container + ffplayService FFplayContract + isAutoRemove bool +} + +func NewItemsToConvert(itemsContainer *fyne.Container, ffplayService FFplayContract, localizerService LocalizerContract) *ItemsToConvert { + containerForItems := container.NewVBox() + ItemsToConvert := &ItemsToConvert{ + nextId: 0, + items: map[int]ItemToConvertContract{}, + itemsContainer: containerForItems, + ffplayService: ffplayService, + isAutoRemove: true, + } + + line := canvas.NewLine(theme.Color(theme.ColorNameFocus)) + line.StrokeWidth = 5 + checkboxAutoRemove := widget.NewCheck(localizerService.GetMessage(&i18n.LocalizeConfig{ + MessageID: "autoClearAfterAddingToQueue", + }), func(checked bool) { + ItemsToConvert.isAutoRemove = checked + }) + checkboxAutoRemove.SetChecked(ItemsToConvert.isAutoRemove) + localizerService.AddChangeCallback("autoClearAfterAddingToQueue", func(text string) { + checkboxAutoRemove.Text = text + }) + + buttonClear := widget.NewButton(localizerService.GetMessage(&i18n.LocalizeConfig{ + MessageID: "clearAll", + }), func() { + ItemsToConvert.clear() + }) + buttonClear.Importance = widget.DangerImportance + localizerService.AddChangeCallback("clearAll", func(text string) { + buttonClear.Text = text + }) + + itemsContainer.Add(container.NewVBox( + container.NewPadded(), + container.NewBorder(nil, nil, nil, buttonClear, container.NewHScroll(checkboxAutoRemove)), + container.NewPadded(), + line, + container.NewPadded(), + containerForItems, + )) + + return ItemsToConvert +} + +func (items *ItemsToConvert) Add(file *File) { + nextId := items.nextId + var content *fyne.Container + var buttonPlay *widget.Button + + buttonPlay = widget.NewButtonWithIcon("", theme.Icon(theme.IconNameMediaPlay), func() { + buttonPlay.Disable() + go func() { + _ = items.ffplayService.Run(FFplaySetting{ + PathToFile: file.Path, + }) + fyne.Do(func() { + buttonPlay.Enable() + }) + }() + }) + + buttonRemove := widget.NewButtonWithIcon("", theme.Icon(theme.IconNameDelete), func() { + items.itemsContainer.Remove(content) + items.itemsContainer.Refresh() + delete(items.items, nextId) + }) + buttonRemove.Importance = widget.DangerImportance + + content = container.NewVBox( + container.NewBorder( + nil, + nil, + buttonPlay, + buttonRemove, + container.NewHScroll(widget.NewLabel(file.Name)), + ), + container.NewHScroll(widget.NewLabel(file.Path)), + container.NewPadded(), + canvas.NewLine(theme.Color(theme.ColorNameFocus)), + container.NewPadded(), + ) + + items.itemsContainer.Add(content) + items.items[nextId] = NewItemToConvert(file, content) + items.nextId++ +} + +func (items *ItemsToConvert) GetItems() map[int]ItemToConvertContract { + return items.items +} + +func (items *ItemsToConvert) AfterAddingQueue() { + if items.isAutoRemove { + items.clear() + } +} + +func (items *ItemsToConvert) clear() { + items.itemsContainer.RemoveAll() + items.items = map[int]ItemToConvertContract{} +} + +type ItemToConvertContract interface { + GetFile() *File + GetContent() *fyne.Container +} + +type ItemToConvert struct { + file *File + content *fyne.Container +} + +func NewItemToConvert(file *File, content *fyne.Container) *ItemToConvert { + return &ItemToConvert{ + file: file, + content: content, + } +} + +func (item ItemToConvert) GetFile() *File { + return item.file +} + +func (item ItemToConvert) GetContent() *fyne.Container { + return item.content +} diff --git a/kernel/layout.go b/kernel/layout.go index a65aeff..3d34179 100644 --- a/kernel/layout.go +++ b/kernel/layout.go @@ -1,8 +1,6 @@ package kernel import ( - "bufio" - "errors" "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" @@ -10,31 +8,31 @@ import ( "fyne.io/fyne/v2/widget" "github.com/nicksnyder/go-i18n/v2/i18n" "image/color" - "io" - "regexp" "strconv" "strings" ) type LayoutContract interface { SetContent(content fyne.CanvasObject) *fyne.Container - NewProgressbar(queueId int, totalDuration float64) ProgressContract ChangeQueueStatus(queueId int, queue *Queue) + GetRightTabs() RightTabsContract } type Layout struct { layout *fyne.Container queueLayoutObject QueueLayoutObjectContract localizerService LocalizerContract + rightTabsService RightTabsContract } -func NewLayout(queueLayoutObject QueueLayoutObjectContract, localizerService LocalizerContract) *Layout { - layout := container.NewAdaptiveGrid(2, widget.NewLabel(""), container.NewVScroll(queueLayoutObject.GetCanvasObject())) +func NewLayout(queueLayoutObject QueueLayoutObjectContract, localizerService LocalizerContract, rightTabsService RightTabsContract) *Layout { + layout := container.NewAdaptiveGrid(2, widget.NewLabel(""), queueLayoutObject.GetCanvasObject()) return &Layout{ layout: layout, queueLayoutObject: queueLayoutObject, localizerService: localizerService, + rightTabsService: rightTabsService, } } @@ -43,18 +41,16 @@ func (l Layout) SetContent(content fyne.CanvasObject) *fyne.Container { return l.layout } -func (l Layout) NewProgressbar(queueId int, totalDuration float64) ProgressContract { - progressbar := l.queueLayoutObject.GetProgressbar(queueId) - return NewProgress(totalDuration, progressbar, l.localizerService) -} - func (l Layout) ChangeQueueStatus(queueId int, queue *Queue) { l.queueLayoutObject.ChangeQueueStatus(queueId, queue) } +func (l Layout) GetRightTabs() RightTabsContract { + return l.rightTabsService +} + type QueueLayoutObjectContract interface { GetCanvasObject() fyne.CanvasObject - GetProgressbar(queueId int) *widget.ProgressBar ChangeQueueStatus(queueId int, queue *Queue) } @@ -63,21 +59,24 @@ type QueueLayoutObject struct { queue QueueListContract container *fyne.Container + containerItems *fyne.Container items map[int]QueueLayoutItem localizerService LocalizerContract queueStatisticsFormat *queueStatisticsFormat + ffplayService FFplayContract } type QueueLayoutItem struct { - CanvasObject fyne.CanvasObject - ProgressBar *widget.ProgressBar - StatusMessage *canvas.Text - MessageError *canvas.Text + CanvasObject fyne.CanvasObject + BlockMessageError *container.Scroll + StatusMessage *canvas.Text + MessageError *canvas.Text + buttonPlay *widget.Button status *StatusContract } -func NewQueueLayoutObject(queue QueueListContract, localizerService LocalizerContract) *QueueLayoutObject { +func NewQueueLayoutObject(queue QueueListContract, localizerService LocalizerContract, ffplayService FFplayContract, rightTabsService RightTabsContract, blockProgressbar *fyne.Container) *QueueLayoutObject { title := widget.NewLabel(localizerService.GetMessage(&i18n.LocalizeConfig{MessageID: "queue"})) title.TextStyle.Bold = true @@ -89,15 +88,31 @@ func NewQueueLayoutObject(queue QueueListContract, localizerService LocalizerCon items := map[int]QueueLayoutItem{} queueStatisticsFormat := newQueueStatisticsFormat(localizerService, &items) + line := canvas.NewLine(theme.Color(theme.ColorNameFocus)) + line.StrokeWidth = 5 + + rightTabsService.GetFileQueueContainer().Add(container.NewVBox( + container.NewPadded(), + container.NewHBox(title, queueStatisticsFormat.completed.widget, queueStatisticsFormat.error.widget), + container.NewHBox(queueStatisticsFormat.inProgress.widget, queueStatisticsFormat.waiting.widget, queueStatisticsFormat.total.widget), + container.NewPadded(), + line, + container.NewPadded(), + )) queueLayoutObject := &QueueLayoutObject{ queue: queue, - container: container.NewVBox( - container.NewHBox(title, queueStatisticsFormat.completed.widget, queueStatisticsFormat.error.widget), - container.NewHBox(queueStatisticsFormat.inProgress.widget, queueStatisticsFormat.waiting.widget, queueStatisticsFormat.total.widget), + container: container.NewBorder( + container.NewVBox( + blockProgressbar, + widget.NewSeparator(), + ), + nil, nil, nil, container.NewVScroll(rightTabsService.GetTabs()), ), + containerItems: rightTabsService.GetFileQueueContainer(), items: items, localizerService: localizerService, queueStatisticsFormat: queueStatisticsFormat, + ffplayService: ffplayService, } queue.AddListener(queueLayoutObject) @@ -109,24 +124,24 @@ func (o QueueLayoutObject) GetCanvasObject() fyne.CanvasObject { return o.container } -func (o QueueLayoutObject) GetProgressbar(queueId int) *widget.ProgressBar { - if item, ok := o.items[queueId]; ok { - return item.ProgressBar - } - return widget.NewProgressBar() -} - func (o QueueLayoutObject) Add(id int, queue *Queue) { - progressBar := widget.NewProgressBar() - statusMessage := canvas.NewText(o.getStatusTitle(queue.Status), theme.Color(theme.ColorNamePrimary)) messageError := canvas.NewText("", theme.Color(theme.ColorNameError)) + buttonPlay := widget.NewButtonWithIcon("", theme.Icon(theme.IconNameMediaPlay), func() { + + }) + buttonPlay.Hide() + blockMessageError := container.NewHScroll(messageError) + blockMessageError.Hide() content := container.NewVBox( container.NewHScroll(widget.NewLabel(queue.Setting.VideoFileInput.Name)), - progressBar, - container.NewHScroll(statusMessage), - container.NewHScroll(messageError), + container.NewHBox( + buttonPlay, + statusMessage, + ), + blockMessageError, + container.NewPadded(), canvas.NewLine(theme.Color(theme.ColorNameFocus)), container.NewPadded(), ) @@ -137,13 +152,14 @@ func (o QueueLayoutObject) Add(id int, queue *Queue) { } o.items[id] = QueueLayoutItem{ - CanvasObject: content, - ProgressBar: progressBar, - StatusMessage: statusMessage, - MessageError: messageError, - status: &queue.Status, + CanvasObject: content, + StatusMessage: statusMessage, + BlockMessageError: blockMessageError, + MessageError: messageError, + buttonPlay: buttonPlay, + status: &queue.Status, } - o.container.Add(content) + o.containerItems.Add(content) } func (o QueueLayoutObject) Remove(id int) { @@ -166,9 +182,24 @@ func (o QueueLayoutObject) ChangeQueueStatus(queueId int, queue *Queue) { item.MessageError.Text = queue.Error.Error() item.MessageError.Color = statusColor fyne.Do(func() { + item.BlockMessageError.Show() item.MessageError.Refresh() }) } + if queue.Status == StatusType(Completed) { + item.buttonPlay.Show() + item.buttonPlay.OnTapped = func() { + item.buttonPlay.Disable() + go func() { + _ = o.ffplayService.Run(FFplaySetting{ + PathToFile: queue.Setting.VideoFileOut.Path, + }) + fyne.Do(func() { + item.buttonPlay.Enable() + }) + }() + } + } if o.queueStatisticsFormat.isChecked(queue.Status) == false && item.CanvasObject.Visible() == true { item.CanvasObject.Hide() } else if item.CanvasObject.Visible() == false { @@ -194,101 +225,6 @@ func (o QueueLayoutObject) getStatusTitle(status StatusContract) string { return o.localizerService.GetMessage(&i18n.LocalizeConfig{MessageID: status.Name() + "Queue"}) } -type Progress struct { - totalDuration float64 - progressbar *widget.ProgressBar - protocol string - localizerService LocalizerContract -} - -func NewProgress(totalDuration float64, progressbar *widget.ProgressBar, localizerService LocalizerContract) Progress { - return Progress{ - totalDuration: totalDuration, - progressbar: progressbar, - protocol: "pipe:", - localizerService: localizerService, - } -} - -func (p Progress) GetProtocole() string { - return p.protocol -} - -func (p Progress) Run(stdOut io.ReadCloser, stdErr io.ReadCloser) error { - isProcessCompleted := false - var errorText string - - p.progressbar.Value = 0 - p.progressbar.Max = p.totalDuration - fyne.Do(func() { - p.progressbar.Refresh() - }) - progress := 0.0 - - go func() { - scannerErr := bufio.NewReader(stdErr) - for { - line, _, err := scannerErr.ReadLine() - if err != nil { - if err == io.EOF { - break - } - continue - } - data := strings.TrimSpace(string(line)) - errorText = data - } - }() - - scannerOut := bufio.NewReader(stdOut) - for { - line, _, err := scannerOut.ReadLine() - if err != nil { - if err == io.EOF { - break - } - continue - } - data := strings.TrimSpace(string(line)) - if strings.Contains(data, "progress=end") { - p.progressbar.Value = p.totalDuration - fyne.Do(func() { - p.progressbar.Refresh() - }) - isProcessCompleted = true - break - } - - re := regexp.MustCompile(`frame=(\d+)`) - a := re.FindAllStringSubmatch(data, -1) - - if len(a) > 0 && len(a[len(a)-1]) > 0 { - c, err := strconv.Atoi(a[len(a)-1][len(a[len(a)-1])-1]) - if err != nil { - continue - } - progress = float64(c) - } - if p.progressbar.Value != progress { - p.progressbar.Value = progress - fyne.Do(func() { - p.progressbar.Refresh() - }) - } - } - - if isProcessCompleted == false { - if len(errorText) == 0 { - errorText = p.localizerService.GetMessage(&i18n.LocalizeConfig{ - MessageID: "errorConverter", - }) - } - return errors.New(errorText) - } - - return nil -} - type queueStatistics struct { widget *widget.Check title string diff --git a/kernel/progressbar.go b/kernel/progressbar.go new file mode 100644 index 0000000..48b8485 --- /dev/null +++ b/kernel/progressbar.go @@ -0,0 +1,254 @@ +package kernel + +import ( + "bufio" + "errors" + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" + "github.com/nicksnyder/go-i18n/v2/i18n" + "image/color" + "io" + "regexp" + "strconv" + "strings" +) + +type BlockProgressbarContract interface { + GetContainer() *fyne.Container + GetProgressbar(totalDuration float64, filePath string, localizerService LocalizerContract) Progress + ProcessEndedWithError(errorText string) + ProcessEndedWithSuccess(filePath string) +} + +type BlockProgressbar struct { + container *fyne.Container + label *widget.Label + progressbar *widget.ProgressBar + errorBlock *container.Scroll + messageError *canvas.Text + statusMessage *canvas.Text + buttonPlay *widget.Button + statusesText *BlockProgressbarStatusesText + ffplayService FFplayContract +} + +func NewBlockProgressbar(statusesText *BlockProgressbarStatusesText, ffplayService FFplayContract) *BlockProgressbar { + label := widget.NewLabel("") + progressbar := widget.NewProgressBar() + + statusMessage := canvas.NewText("", theme.Color(theme.ColorNamePrimary)) + messageError := canvas.NewText("", theme.Color(theme.ColorNameError)) + buttonPlay := widget.NewButtonWithIcon("", theme.Icon(theme.IconNameMediaPlay), func() { + + }) + buttonPlay.Hide() + + errorBlock := container.NewHScroll(messageError) + errorBlock.Hide() + + content := container.NewVBox( + container.NewHScroll(label), + progressbar, + container.NewHScroll(container.NewHBox( + buttonPlay, + statusMessage, + )), + errorBlock, + ) + content.Hide() + + return &BlockProgressbar{ + container: content, + label: label, + progressbar: progressbar, + errorBlock: errorBlock, + messageError: messageError, + statusMessage: statusMessage, + buttonPlay: buttonPlay, + statusesText: statusesText, + ffplayService: ffplayService, + } +} + +func (block BlockProgressbar) GetContainer() *fyne.Container { + return block.container +} + +func (block BlockProgressbar) GetProgressbar(totalDuration float64, filePath string, localizerService LocalizerContract) Progress { + block.label.Text = filePath + block.statusMessage.Color = theme.Color(theme.ColorNamePrimary) + block.statusMessage.Text = block.statusesText.inProgress + block.messageError.Text = "" + fyne.Do(func() { + block.buttonPlay.Hide() + if block.errorBlock.Visible() { + block.errorBlock.Hide() + } + block.statusMessage.Refresh() + block.container.Refresh() + block.errorBlock.Refresh() + }) + + block.progressbar.Value = 0 + return NewProgress(totalDuration, block.progressbar, localizerService) +} + +func (block BlockProgressbar) ProcessEndedWithError(errorText string) { + fyne.Do(func() { + block.statusMessage.Color = theme.Color(theme.ColorNameError) + block.statusMessage.Text = block.statusesText.error + block.messageError.Text = errorText + block.errorBlock.Show() + }) +} + +func (block BlockProgressbar) ProcessEndedWithSuccess(filePath string) { + fyne.Do(func() { + block.statusMessage.Color = color.RGBA{R: 49, G: 127, B: 114, A: 255} + block.statusMessage.Text = block.statusesText.completed + block.buttonPlay.Show() + block.buttonPlay.OnTapped = func() { + block.buttonPlay.Disable() + go func() { + _ = block.ffplayService.Run(FFplaySetting{ + PathToFile: filePath, + }) + fyne.Do(func() { + block.buttonPlay.Enable() + }) + }() + } + }) +} + +type Progress struct { + totalDuration float64 + progressbar *widget.ProgressBar + protocol string + localizerService LocalizerContract +} + +func NewProgress(totalDuration float64, progressbar *widget.ProgressBar, localizerService LocalizerContract) Progress { + return Progress{ + totalDuration: totalDuration, + progressbar: progressbar, + protocol: "pipe:", + localizerService: localizerService, + } +} + +func (p Progress) GetProtocole() string { + return p.protocol +} + +func (p Progress) Run(stdOut io.ReadCloser, stdErr io.ReadCloser) error { + isProcessCompleted := false + var errorText string + + p.progressbar.Value = 0 + p.progressbar.Max = p.totalDuration + fyne.Do(func() { + p.progressbar.Refresh() + }) + progress := 0.0 + + go func() { + scannerErr := bufio.NewReader(stdErr) + for { + line, _, err := scannerErr.ReadLine() + if err != nil { + if err == io.EOF { + break + } + continue + } + data := strings.TrimSpace(string(line)) + errorText = data + } + }() + + scannerOut := bufio.NewReader(stdOut) + for { + line, _, err := scannerOut.ReadLine() + if err != nil { + if err == io.EOF { + break + } + continue + } + data := strings.TrimSpace(string(line)) + if strings.Contains(data, "progress=end") { + p.progressbar.Value = p.totalDuration + fyne.Do(func() { + p.progressbar.Refresh() + }) + isProcessCompleted = true + break + } + + re := regexp.MustCompile(`frame=(\d+)`) + a := re.FindAllStringSubmatch(data, -1) + + if len(a) > 0 && len(a[len(a)-1]) > 0 { + c, err := strconv.Atoi(a[len(a)-1][len(a[len(a)-1])-1]) + if err != nil { + continue + } + progress = float64(c) + } + if p.progressbar.Value != progress { + p.progressbar.Value = progress + fyne.Do(func() { + p.progressbar.Refresh() + }) + } + } + + if isProcessCompleted == false { + if len(errorText) == 0 { + errorText = p.localizerService.GetMessage(&i18n.LocalizeConfig{ + MessageID: "errorConverter", + }) + } + return errors.New(errorText) + } + + return nil +} + +type BlockProgressbarStatusesText struct { + inProgress string + completed string + error string +} + +func GetBlockProgressbarStatusesText(localizerService LocalizerContract) *BlockProgressbarStatusesText { + statusesText := &BlockProgressbarStatusesText{ + inProgress: localizerService.GetMessage(&i18n.LocalizeConfig{ + MessageID: "inProgressQueue", + }), + completed: localizerService.GetMessage(&i18n.LocalizeConfig{ + MessageID: "completedQueue", + }), + error: localizerService.GetMessage(&i18n.LocalizeConfig{ + MessageID: "errorQueue", + }), + } + + localizerService.AddChangeCallback("inProgressQueue", func(text string) { + statusesText.inProgress = text + }) + + localizerService.AddChangeCallback("completedQueue", func(text string) { + statusesText.completed = text + }) + + localizerService.AddChangeCallback("errorQueue", func(text string) { + statusesText.error = text + }) + + return statusesText +} diff --git a/kernel/right_tabs.go b/kernel/right_tabs.go new file mode 100644 index 0000000..16f8de1 --- /dev/null +++ b/kernel/right_tabs.go @@ -0,0 +1,76 @@ +package kernel + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "github.com/nicksnyder/go-i18n/v2/i18n" +) + +type RightTabsContract interface { + GetTabs() *container.AppTabs + GetAddedFilesContainer() *fyne.Container + GetFileQueueContainer() *fyne.Container + SelectFileQueueTab() + SelectAddedFilesTab() +} + +type RightTabs struct { + tabs *container.AppTabs + + addedFilesContainer *fyne.Container + addedFilesTab *container.TabItem + + fileQueueContainer *fyne.Container + fileQueueTab *container.TabItem +} + +func NewRightTabs(localizerService LocalizerContract) *RightTabs { + addedFilesContainer := container.NewVBox() + addedFilesTab := container.NewTabItem(localizerService.GetMessage(&i18n.LocalizeConfig{MessageID: "addedFilesTitle"}), addedFilesContainer) + localizerService.AddChangeCallback("addedFilesTitle", func(text string) { + addedFilesTab.Text = text + }) + + fileQueueContainer := container.NewVBox() + fileQueueTab := container.NewTabItem(localizerService.GetMessage(&i18n.LocalizeConfig{MessageID: "fileQueueTitle"}), fileQueueContainer) + localizerService.AddChangeCallback("fileQueueTitle", func(text string) { + fileQueueTab.Text = text + }) + + tabs := container.NewAppTabs( + addedFilesTab, + fileQueueTab, + ) + + return &RightTabs{ + tabs: tabs, + addedFilesContainer: addedFilesContainer, + addedFilesTab: addedFilesTab, + fileQueueContainer: fileQueueContainer, + fileQueueTab: fileQueueTab, + } +} + +func (t RightTabs) GetTabs() *container.AppTabs { + return t.tabs +} + +func (t RightTabs) GetAddedFilesContainer() *fyne.Container { + return t.addedFilesContainer +} + +func (t RightTabs) GetFileQueueContainer() *fyne.Container { + return t.fileQueueContainer +} + +func (t RightTabs) SelectFileQueueTab() { + fyne.Do(func() { + t.tabs.Select(t.fileQueueTab) + }) +} + +func (t RightTabs) SelectAddedFilesTab() { + fyne.Do(func() { + t.tabs.Select(t.addedFilesTab) + }) +} diff --git a/kernel/window.go b/kernel/window.go index 7bc565c..cec7b5b 100644 --- a/kernel/window.go +++ b/kernel/window.go @@ -83,5 +83,7 @@ func (w Window) GetLayout() LayoutContract { } func (w Window) SetOnDropped(callback func(position fyne.Position, uris []fyne.URI)) { - w.windowFyne.SetOnDropped(callback) + fyne.Do(func() { + w.windowFyne.SetOnDropped(callback) + }) } diff --git a/languages/active.en.toml b/languages/active.en.toml index fc2bb35..c5c158d 100644 --- a/languages/active.en.toml +++ b/languages/active.en.toml @@ -10,6 +10,14 @@ other = "About" hash = "sha1-8bd565814118ba8b90c40eb5b62acf8d2676e7d6" other = "A simple interface for the FFmpeg console utility. \nBut I am not the author of the FFmpeg utility itself." +[addedFilesTitle] +hash = "sha1-8ba0f6e477b0d78df2cc06f1d8b41b888623b851" +other = "Added files" + +[autoClearAfterAddingToQueue] +hash = "sha1-b3781695a4c35380d2cd075bb52f27a2a6d8f19c" +other = "Auto-clear after adding to queue" + [buttonDownloadFFmpeg] hash = "sha1-c223c2e15171156192bc3146aee91e6094bb475b" other = "Download FFmpeg automatically" @@ -23,8 +31,8 @@ hash = "sha1-0ec753be8df955a117404fb634b01b45eb386e2a" other = "Cancel" [changeFFPath] -hash = "sha1-46793a2844600d0eb19fa3540fb9564ee5705491" -other = "FFmpeg and FFprobe" +hash = "sha1-1f704de0560f8135eb6924cd232ed919ca2e5af0" +other = "FFmpeg, FFprobe and FFplay" [changeLanguage] hash = "sha1-8b276eaf378d485c769fb3d5dcc06dfc25b0c01b" @@ -38,6 +46,10 @@ other = "Allow file to be overwritten" hash = "sha1-f60bb5f761024d973834b5e9d25ceebce2c85f94" other = "choose" +[clearAll] +hash = "sha1-f32702d79ac206432400ac6b041695d020f6fa77" +other = "Clear List" + [completedQueue] hash = "sha1-398c7d4f7b0d522afb930769c0fbb1a9f4b61fbe" other = "Completed" @@ -62,9 +74,9 @@ other = "Will be downloaded from the site:" hash = "sha1-55f87f114628fa2d5d8e67d1e1cda22c0e4f9271" other = "Downloading..." -[dragAndDrop1File] -hash = "sha1-7259670822df1cc92ef5f06ed3c0e9407746975a" -other = "drag and drop 1 file" +[dragAndDropFiles] +hash = "sha1-07bb747cc7590d7a51cdf96dff49a74139097766" +other = "drag and drop files" [encoderGroupAudio] hash = "sha1-24321cb5400df96be8f3e2131918bebdb3a01bba" @@ -222,9 +234,13 @@ other = "Couldn't convert video" hash = "sha1-531abc3f0d12727e542df6e5a22de91098380fc1" other = "could not create file 'database' in folder 'data'" -[errorDragAndDrop1File] -hash = "sha1-a8edb5cbd622f3ce4ec07a2377e22ec5fad4491b" -other = "You can only drag and drop 1 file." +[errorDatabaseTimeout] +hash = "sha1-f8153516ac2442d19be4b6daccce839d204ff09f" +other = "Could not open configuration file.\nMake sure another copy of the program is not running!" + +[errorDragAndDropFile] +hash = "sha1-863cf1ad9c820d5b0c2006ceeaa29e25f81c1714" +other = "Not all files were added" [errorFFmpeg] hash = "sha1-ccf0b95c0d1b392dc215258d917eb4e5d0b88ed0" @@ -234,6 +250,14 @@ other = "this is not FFmpeg" hash = "sha1-9a4148d42186b6b32cf83bef726e23022c53283f" other = "Could not determine FFmpeg version" +[errorFFplay] +hash = "sha1-988122112ac6002094e25518cfb5f0d606217298" +other = "this is not FFplay" + +[errorFFplayVersion] +hash = "sha1-cd60928d20d93210e103dd464306ab138bf1b184" +other = "Could not determine FFplay version" + [errorFFprobe] hash = "sha1-86d1b0b4c4ccd6a4f71e758fc67ce11aff4ba9b8" other = "this is not FFprobe" @@ -242,9 +266,9 @@ other = "this is not FFprobe" hash = "sha1-da7b37d7df3fafbd153665b13888413d52b24c17" other = "Failed to determine FFprobe version" -[errorIsFolder] -hash = "sha1-f937d090b6e320957514d850657cdf2f911dc6aa" -other = "You can only drag and drop a file" +[errorNoFilesAddedForConversion] +hash = "sha1-5cf1f65bef15cb0382e56be98f44c6abde56a314" +other = "There are no files to convert" [errorQueue] hash = "sha1-72aecd9ad85642d84d62dbbf3cf70953c5f696c7" @@ -278,6 +302,10 @@ other = "**FFmpeg** is a trademark of **[Fabrice Bellard](http://bellard.org/)** hash = "sha1-96ac799e1086b31fd8f5f8d4c801829d6c853f08" other = "File:" +[fileQueueTitle] +hash = "sha1-aec93b16baeaf55fed871075c9494a460e4a91b8" +other = "Queue" + [formPreset] hash = "sha1-7759891ba1ef9f7adc70defc7ac18fbf149c1a68" other = "Preset" @@ -294,6 +322,102 @@ other = "I sincerely thank you for your invaluable\n\r and timely assistance:" hash = "sha1-6a45cef900c668effcb2ab10da05855c1fd10f6f" other = "Help" +[helpFFplay] +hash = "sha1-ecc294b8b3d217ee1c2d63dc2f0253c3d1b3712c" +other = "FFplay Player Keys" + +[helpFFplayActivateFrameStepMode] +hash = "sha1-f47ede90932d69465f6197cb2a7cc4d1e3ab150e" +other = "Activate frame-by-frame mode." + +[helpFFplayCycleVideoFiltersOrShowModes] +hash = "sha1-83bb702c777e4768cdc326a668d541c23ab759b7" +other = "A cycle of video filters or display modes." + +[helpFFplayDecreaseVolume] +hash = "sha1-de28db96a9c22be885ec5067a13f8f17fd3954bc" +other = "Decrease the volume." + +[helpFFplayDescription] +hash = "sha1-f5441f6aee76222c4120066575e80c2d177ac3c0" +other = "Description" + +[helpFFplayDoubleClickLeftMouseButton] +hash = "sha1-2657aa576055769952dfcde570fc9b4765d594ad" +other = "double click\nleft mouse button" + +[helpFFplayIncreaseVolume] +hash = "sha1-8ba7bde2d9a80f4a7cd122cf4973975698d3bd34" +other = "Increase the volume." + +[helpFFplayKeyDown] +hash = "sha1-c5aefd2f8c6908a69b08fe4a2d235b1ae0113470" +other = "down" + +[helpFFplayKeyHoldS] +hash = "sha1-89c5dd8287c15b3f40db66e06b038c34a715f02f" +other = "hold S" + +[helpFFplayKeyLeft] +hash = "sha1-feb671890703fb0300a436744d34018bbc7ba13a" +other = "left" + +[helpFFplayKeyRight] +hash = "sha1-a4f025d4bf7f90ee5bec6c48b2710bc9c5bbb267" +other = "right" + +[helpFFplayKeySpace] +hash = "sha1-a367ad00358ec44edc1d54a96df6f9114b0f8697" +other = "SPACE" + +[helpFFplayKeyUp] +hash = "sha1-e4845aa8c0e100a80eaf65446c59085236fd2098" +other = "up" + +[helpFFplayKeys] +hash = "sha1-0ad272ade8c568f394499f1492ecfab56e701e5d" +other = "Keys" + +[helpFFplayPause] +hash = "sha1-e83e107900fde0c39295f599c2cf8fba8d8cb604" +other = "Pause or continue playing." + +[helpFFplayQuit] +hash = "sha1-70785a2fd5d5a6519b7439f0d8cfcd7d54c5771d" +other = "Close the player." + +[helpFFplaySeekBForward10Minutes] +hash = "sha1-58ed63343376240f2596e447b5245c1805f35234" +other = "Fast forward 10 minutes." + +[helpFFplaySeekBForward1Minute] +hash = "sha1-3fe46b8d5413b7fdc53ae9ed9427bcb1769ec74c" +other = "Fast forward 1 minute." + +[helpFFplaySeekBackward10Minutes] +hash = "sha1-927dffe9af72ffd40f46873b452a4c90627bccf8" +other = "Rewind 10 minutes." + +[helpFFplaySeekBackward10Seconds] +hash = "sha1-e97615ecec0f8cf5647e8802bdda38dc2b0d809f" +other = "Rewind 10 seconds." + +[helpFFplaySeekBackward1Minute] +hash = "sha1-5b19e280a0850122c8ebc80c622491bb09520e1a" +other = "Rewind 1 minute." + +[helpFFplaySeekForward10Seconds] +hash = "sha1-8d840251d4a1668edaea3515df197a8a79031ec3" +other = "Fast forward 10 seconds." + +[helpFFplayToggleFullScreen] +hash = "sha1-d32df02849258c5b02f15e5711f54ee6a8a75fd4" +other = "Switch to full screen or exit full screen." + +[helpFFplayToggleMute] +hash = "sha1-4bdbb124fe8de3a8037c1e74719e9600b21b25ab" +other = "Mute or unmute." + [inProgressQueue] hash = "sha1-eff79c40e2100ae5fadf3a7d99336025edcca8b5" other = "In Progress" @@ -314,6 +438,14 @@ other = "License information" hash = "sha1-359fff328717c05104e51a2d29f05bf1875d26b7" other = "Licenses from other products used in the program" +[menuSettingsLanguage] +hash = "sha1-ed3f0e507a5b4ed0649d7c768fe0d47413d839ba" +other = "Language" + +[menuSettingsTheme] +hash = "sha1-553c45f1b84a92b08dc1f088c13f924cde95765e" +other = "Theme" + [or] hash = "sha1-30bb0333ca1583110e4ced513b5d2455b86f529b" other = "or" @@ -326,6 +458,10 @@ other = "Enable option" hash = "sha1-fafc50f1db0f720fe83a96cd70a9e1ad824e96b6" other = "Path to FFmpeg:" +[pathToFfplay] +hash = "sha1-5389830dd75a63aa8a5e41e8f07c5fadd8385398" +other = "Path to FFplay:" + [pathToFfprobe] hash = "sha1-b872edc9633a2e81ef678dc46fe46a7e91732024" other = "Path to FFprobe:" @@ -406,6 +542,18 @@ other = "Settings" hash = "sha1-f5b8ed88e9609963035d2235be0a79bbec619976" other = "Checking FFmpeg for serviceability..." +[themesNameDark] +hash = "sha1-bd16b234708a2515a9f2d0ca41fb11e7fe8a38a2" +other = "Dark" + +[themesNameDefault] +hash = "sha1-469631cb165dcbbfea9e747056c25fbccb28c481" +other = "Default" + +[themesNameLight] +hash = "sha1-8080010c5e7d7edf56e89a99d8a2422898417845" +other = "Light" + [titleDownloadLink] hash = "sha1-92df86371f6c3a06ca1e4754f113142776a32d49" other = "You can download it from here" diff --git a/languages/active.kk.toml b/languages/active.kk.toml index c17d909..2074652 100644 --- a/languages/active.kk.toml +++ b/languages/active.kk.toml @@ -10,6 +10,14 @@ other = "Бағдарлама туралы" hash = "sha1-8bd565814118ba8b90c40eb5b62acf8d2676e7d6" other = "FFmpeg консоль утилитасы үшін қарапайым интерфейс. \nБірақ мен FFmpeg утилитасының авторы емеспін." +[addedFilesTitle] +hash = "sha1-8ba0f6e477b0d78df2cc06f1d8b41b888623b851" +other = "Қосылған файлдар" + +[autoClearAfterAddingToQueue] +hash = "sha1-b3781695a4c35380d2cd075bb52f27a2a6d8f19c" +other = "Кезекке қосқаннан кейін тазалаңыз" + [buttonDownloadFFmpeg] hash = "sha1-c223c2e15171156192bc3146aee91e6094bb475b" other = "FFmpeg автоматты түрде жүктеп алыңыз" @@ -23,8 +31,8 @@ hash = "sha1-0ec753be8df955a117404fb634b01b45eb386e2a" other = "Болдырмау" [changeFFPath] -hash = "sha1-46793a2844600d0eb19fa3540fb9564ee5705491" -other = "FFmpeg және FFprobe" +hash = "sha1-1f704de0560f8135eb6924cd232ed919ca2e5af0" +other = "FFmpeg, FFprobe және FFplay" [changeLanguage] hash = "sha1-8b276eaf378d485c769fb3d5dcc06dfc25b0c01b" @@ -38,6 +46,10 @@ other = "Файлды қайта жазуға рұқсат беріңіз" hash = "sha1-f60bb5f761024d973834b5e9d25ceebce2c85f94" other = "таңдау" +[clearAll] +hash = "sha1-f32702d79ac206432400ac6b041695d020f6fa77" +other = "Тізімді өшіру" + [completedQueue] hash = "sha1-398c7d4f7b0d522afb930769c0fbb1a9f4b61fbe" other = "Дайын" @@ -62,9 +74,9 @@ other = "Сайттан жүктеледі:" hash = "sha1-55f87f114628fa2d5d8e67d1e1cda22c0e4f9271" other = "Жүктеп алынуда..." -[dragAndDrop1File] -hash = "sha1-7259670822df1cc92ef5f06ed3c0e9407746975a" -other = "1 файлды сүйреңіз" +[dragAndDropFiles] +hash = "sha1-07bb747cc7590d7a51cdf96dff49a74139097766" +other = "файлдарды сүйреп апарыңыз" [encoderGroupAudio] hash = "sha1-24321cb5400df96be8f3e2131918bebdb3a01bba" @@ -222,9 +234,13 @@ other = "Бейнені түрлендіру мүмкін болмады" hash = "sha1-531abc3f0d12727e542df6e5a22de91098380fc1" other = "'data' қалтасында 'database' файлын жасау мүмкін болмады" -[errorDragAndDrop1File] -hash = "sha1-a8edb5cbd622f3ce4ec07a2377e22ec5fad4491b" -other = "Тек 1 файлды сүйреп апаруға болады" +[errorDatabaseTimeout] +hash = "sha1-f8153516ac2442d19be4b6daccce839d204ff09f" +other = "Конфигурация файлын аша алмады.\nБағдарламаның басқа көшірмесі іске қосылмағанына көз жеткізіңіз!" + +[errorDragAndDropFile] +hash = "sha1-863cf1ad9c820d5b0c2006ceeaa29e25f81c1714" +other = "Барлық файлдар қосылмаған" [errorFFmpeg] hash = "sha1-ccf0b95c0d1b392dc215258d917eb4e5d0b88ed0" @@ -234,6 +250,14 @@ other = "бұл FFmpeg емес" hash = "sha1-9a4148d42186b6b32cf83bef726e23022c53283f" other = "FFmpeg нұсқасын анықтау мүмкін болмады" +[errorFFplay] +hash = "sha1-988122112ac6002094e25518cfb5f0d606217298" +other = "бұл FFplay емес" + +[errorFFplayVersion] +hash = "sha1-cd60928d20d93210e103dd464306ab138bf1b184" +other = "FFplay нұсқасын анықтау мүмкін болмады" + [errorFFprobe] hash = "sha1-86d1b0b4c4ccd6a4f71e758fc67ce11aff4ba9b8" other = "бұл FFprobe емес" @@ -242,9 +266,9 @@ other = "бұл FFprobe емес" hash = "sha1-da7b37d7df3fafbd153665b13888413d52b24c17" other = "FFprobe нұсқасын анықтау мүмкін болмады" -[errorIsFolder] -hash = "sha1-f937d090b6e320957514d850657cdf2f911dc6aa" -other = "Тек файлды сүйреп апаруға болады" +[errorNoFilesAddedForConversion] +hash = "sha1-5cf1f65bef15cb0382e56be98f44c6abde56a314" +other = "Түрлендіруге арналған файлдар жоқ" [errorQueue] hash = "sha1-72aecd9ad85642d84d62dbbf3cf70953c5f696c7" @@ -278,6 +302,10 @@ other = "FFmpeg — **[FFmpeg](https://ffmpeg.org/about.html)** жобасын hash = "sha1-96ac799e1086b31fd8f5f8d4c801829d6c853f08" other = "Файл:" +[fileQueueTitle] +hash = "sha1-aec93b16baeaf55fed871075c9494a460e4a91b8" +other = "Кезек" + [formPreset] hash = "sha1-7759891ba1ef9f7adc70defc7ac18fbf149c1a68" other = "Алдын ала орнатылған" @@ -294,6 +322,102 @@ other = "Сізге баға жетпес және уақтылы көмекте hash = "sha1-6a45cef900c668effcb2ab10da05855c1fd10f6f" other = "Анықтама" +[helpFFplay] +hash = "sha1-ecc294b8b3d217ee1c2d63dc2f0253c3d1b3712c" +other = "FFplay ойнатқышының пернелері" + +[helpFFplayActivateFrameStepMode] +hash = "sha1-f47ede90932d69465f6197cb2a7cc4d1e3ab150e" +other = "Уақыт аралығын іске қосыңыз." + +[helpFFplayCycleVideoFiltersOrShowModes] +hash = "sha1-83bb702c777e4768cdc326a668d541c23ab759b7" +other = "Бейне сүзгілерінің немесе дисплей режимдерінің циклі." + +[helpFFplayDecreaseVolume] +hash = "sha1-de28db96a9c22be885ec5067a13f8f17fd3954bc" +other = "Дыбыс деңгейін төмендетіңіз." + +[helpFFplayDescription] +hash = "sha1-f5441f6aee76222c4120066575e80c2d177ac3c0" +other = "Сипаттама" + +[helpFFplayDoubleClickLeftMouseButton] +hash = "sha1-2657aa576055769952dfcde570fc9b4765d594ad" +other = "тінтуірдің сол жақ\nбатырмасын екі рет басу" + +[helpFFplayIncreaseVolume] +hash = "sha1-8ba7bde2d9a80f4a7cd122cf4973975698d3bd34" +other = "Дыбыс деңгейін арттыру." + +[helpFFplayKeyDown] +hash = "sha1-c5aefd2f8c6908a69b08fe4a2d235b1ae0113470" +other = "төмен" + +[helpFFplayKeyHoldS] +hash = "sha1-89c5dd8287c15b3f40db66e06b038c34a715f02f" +other = "ұстау S" + +[helpFFplayKeyLeft] +hash = "sha1-feb671890703fb0300a436744d34018bbc7ba13a" +other = "сол" + +[helpFFplayKeyRight] +hash = "sha1-a4f025d4bf7f90ee5bec6c48b2710bc9c5bbb267" +other = "құқық" + +[helpFFplayKeySpace] +hash = "sha1-a367ad00358ec44edc1d54a96df6f9114b0f8697" +other = "SPACE (пробел)" + +[helpFFplayKeyUp] +hash = "sha1-e4845aa8c0e100a80eaf65446c59085236fd2098" +other = "жоғары" + +[helpFFplayKeys] +hash = "sha1-0ad272ade8c568f394499f1492ecfab56e701e5d" +other = "Кілттер" + +[helpFFplayPause] +hash = "sha1-e83e107900fde0c39295f599c2cf8fba8d8cb604" +other = "Кідіртіңіз немесе жоғалтуды жалғастырыңыз." + +[helpFFplayQuit] +hash = "sha1-70785a2fd5d5a6519b7439f0d8cfcd7d54c5771d" +other = "Ойнатқышты жабыңыз." + +[helpFFplaySeekBForward10Minutes] +hash = "sha1-58ed63343376240f2596e447b5245c1805f35234" +other = "10 минутқа алға айналдырыңыз." + +[helpFFplaySeekBForward1Minute] +hash = "sha1-3fe46b8d5413b7fdc53ae9ed9427bcb1769ec74c" +other = "1 минутқа алға айналдырыңыз." + +[helpFFplaySeekBackward10Minutes] +hash = "sha1-927dffe9af72ffd40f46873b452a4c90627bccf8" +other = "10 минутқа артқа айналдырыңыз." + +[helpFFplaySeekBackward10Seconds] +hash = "sha1-e97615ecec0f8cf5647e8802bdda38dc2b0d809f" +other = "10 секундқа артқа айналдырыңыз." + +[helpFFplaySeekBackward1Minute] +hash = "sha1-5b19e280a0850122c8ebc80c622491bb09520e1a" +other = "1 минутқа артқа айналдырыңыз." + +[helpFFplaySeekForward10Seconds] +hash = "sha1-8d840251d4a1668edaea3515df197a8a79031ec3" +other = "10 секунд алға айналдырыңыз." + +[helpFFplayToggleFullScreen] +hash = "sha1-d32df02849258c5b02f15e5711f54ee6a8a75fd4" +other = "Толық экранға ауысу немесе толық экраннан шығу." + +[helpFFplayToggleMute] +hash = "sha1-4bdbb124fe8de3a8037c1e74719e9600b21b25ab" +other = "Дыбысты өшіріңіз немесе дыбысты қосыңыз." + [inProgressQueue] hash = "sha1-eff79c40e2100ae5fadf3a7d99336025edcca8b5" other = "Орындалуда" @@ -314,6 +438,14 @@ other = "Лицензия туралы ақпарат" hash = "sha1-359fff328717c05104e51a2d29f05bf1875d26b7" other = "Бағдарламада пайдаланылатын басқа өнімдердің лицензиялары" +[menuSettingsLanguage] +hash = "sha1-ed3f0e507a5b4ed0649d7c768fe0d47413d839ba" +other = "Тіл" + +[menuSettingsTheme] +hash = "sha1-553c45f1b84a92b08dc1f088c13f924cde95765e" +other = "Тақырып" + [or] hash = "sha1-30bb0333ca1583110e4ced513b5d2455b86f529b" other = "немесе" @@ -326,6 +458,10 @@ other = "Опцияны қосу" hash = "sha1-fafc50f1db0f720fe83a96cd70a9e1ad824e96b6" other = "FFmpeg жол:" +[pathToFfplay] +hash = "sha1-5389830dd75a63aa8a5e41e8f07c5fadd8385398" +other = "FFplay жол:" + [pathToFfprobe] hash = "sha1-b872edc9633a2e81ef678dc46fe46a7e91732024" other = "FFprobe жол:" @@ -406,6 +542,18 @@ other = "Параметрлер" hash = "sha1-f5b8ed88e9609963035d2235be0a79bbec619976" other = "FFmpeg функционалдығы тексерілуде..." +[themesNameDark] +hash = "sha1-bd16b234708a2515a9f2d0ca41fb11e7fe8a38a2" +other = "Қараңғы тақырып" + +[themesNameDefault] +hash = "sha1-469631cb165dcbbfea9e747056c25fbccb28c481" +other = "Әдепкі бойынша" + +[themesNameLight] +hash = "sha1-8080010c5e7d7edf56e89a99d8a2422898417845" +other = "Жеңіл тақырып" + [titleDownloadLink] hash = "sha1-92df86371f6c3a06ca1e4754f113142776a32d49" other = "Сіз оны осы жерден жүктей аласыз" diff --git a/languages/active.ru.toml b/languages/active.ru.toml index 39e1679..2383c00 100644 --- a/languages/active.ru.toml +++ b/languages/active.ru.toml @@ -1,20 +1,23 @@ AlsoUsedProgram = "Также в программе используется:" about = "О программе" aboutText = "Простенький интерфейс для консольной утилиты FFmpeg. \nНо я не являюсь автором самой утилиты FFmpeg." +addedFilesTitle = "Добавленные файлы" +autoClearAfterAddingToQueue = "Очищать после добавления в очередь" buttonDownloadFFmpeg = "Скачать автоматически FFmpeg" buttonForSelectedDirTitle = "Сохранить в папку:" cancel = "Отмена" -changeFFPath = "FFmpeg и FFprobe" +changeFFPath = "FFmpeg, FFprobe и FFplay" changeLanguage = "Поменять язык" checkboxOverwriteOutputFilesTitle = "Разрешить перезаписать файл" choose = "выбрать" +clearAll = "Очистить список" completedQueue = "Готово" converterVideoFilesSubmitTitle = "Конвертировать" converterVideoFilesTitle = "Конвертер видео, аудио и картинок" download = "Скачать" downloadFFmpegFromSite = "Будет скачано с сайта:" downloadRun = "Скачивается..." -dragAndDrop1File = "перетащить 1 файл" +dragAndDropFiles = "перетащить файлы" encoderGroupAudio = "Аудио" encoderGroupImage = "Картинки" encoderGroupVideo = "Видео" @@ -54,12 +57,15 @@ encoder_xbm = "XBM (X BitMap) image" error = "Произошла ошибка!" errorConverter = "не смогли отконвертировать видео" errorDatabase = "не смогли создать файл 'database' в папке 'data'" -errorDragAndDrop1File = "Можно перетащить только 1 файл" +errorDatabaseTimeout = "Не смогли открыть файл конфигурации.\nУбедитесь, что другая копия программы не запущена!" +errorDragAndDropFile = "Не все файлы добавились" errorFFmpeg = "это не FFmpeg" errorFFmpegVersion = "Не смогли определить версию FFmpeg" +errorFFplay = "это не FFplay" +errorFFplayVersion = "Не смогли определить версию FFplay" errorFFprobe = "это не FFprobe" errorFFprobeVersion = "Не смогли определить версию FFprobe" -errorIsFolder = "Можно перетаскивать только файл" +errorNoFilesAddedForConversion = "Нет файлов для конвертации" errorQueue = "Ошибка" errorSelectedEncoder = "Конвертер не выбран" errorSelectedFolderSave = "Папка для сохранения не выбрана!" @@ -68,18 +74,46 @@ exit = "Выход" ffmpegLGPL = "Это программное обеспечение использует библиотеки из проекта **FFmpeg** под **[LGPLv2.1](http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html)**." ffmpegTrademark = "**FFmpeg** — торговая марка **[Fabrice Bellard](http://bellard.org/)** , создателя проекта **[FFmpeg](https://ffmpeg.org/about.html)**." fileForConversionTitle = "Файл:" +fileQueueTitle = "Очередь" formPreset = "Предустановка" gratitude = "Благодарность" gratitudeText = "Я искренне благодарю вас за неоценимую\n\rи своевременную помощь:" help = "Справка" +helpFFplay = "Клавиши проигрывателя FFplay" +helpFFplayActivateFrameStepMode = "Активировать покадровый режим." +helpFFplayCycleVideoFiltersOrShowModes = "Цикл видеофильтров или режимов показа." +helpFFplayDecreaseVolume = "Уменьшить громкость." +helpFFplayDescription = "Описание" +helpFFplayDoubleClickLeftMouseButton = "двойной щелчок\nлевой кнопкой мыши" +helpFFplayIncreaseVolume = "Увеличить громкость." +helpFFplayKeyDown = "вниз" +helpFFplayKeyHoldS = "держать S" +helpFFplayKeyLeft = "лево" +helpFFplayKeyRight = "право" +helpFFplayKeySpace = "SPACE (пробел)" +helpFFplayKeyUp = "вверх" +helpFFplayKeys = "Клавиши" +helpFFplayPause = "Поставить на паузу или продолжить проигрывать." +helpFFplayQuit = "Закрыть проигрыватель." +helpFFplaySeekBForward10Minutes = "Перемотать вперёд на 10 минут." +helpFFplaySeekBForward1Minute = "Перемотать вперёд на 1 минуту." +helpFFplaySeekBackward10Minutes = "Перемотать назад на 10 минут." +helpFFplaySeekBackward10Seconds = "Перемотать назад на 10 секунд." +helpFFplaySeekBackward1Minute = "Перемотать назад на 1 минуту." +helpFFplaySeekForward10Seconds = "Перемотать вперёд на 10 секунд." +helpFFplayToggleFullScreen = "Переключиться на полный экран или выйти с полного экрана." +helpFFplayToggleMute = "Отключить звук или включить звук." inProgressQueue = "Выполняется" languageSelectionFormHead = "Переключить язык" languageSelectionHead = "Выберите язык" licenseLink = "Сведения о лицензии" licenseLinkOther = "Лицензии от других продуктов, которые используются в программе" +menuSettingsLanguage = "Язык" +menuSettingsTheme = "Тема" or = "или" parameterCheckbox = "Включить параметр" pathToFfmpeg = "Путь к FFmpeg:" +pathToFfplay = "Путь к FFplay:" pathToFfprobe = "Путь к FFprobe:" preset_fast = "fast (медленней чем faster, но будет файл и меньше весить)" preset_faster = "faster (медленней чем veryfast, но будет файл и меньше весить)" @@ -100,6 +134,9 @@ selectFFPathTitle = "Укажите путь к FFmpeg и к FFprobe" selectFormat = "Расширение файла:" settings = "Настройки" testFF = "Проверка FFmpeg на работоспособность..." +themesNameDark = "Тёмная" +themesNameDefault = "По умолчанию" +themesNameLight = "Светлая" titleDownloadLink = "Скачать можно от сюда" total = "Всего" unzipRun = "Распаковывается..." diff --git a/main.go b/main.go index 2294a2b..5f30405 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,7 @@ import ( "git.kor-elf.net/kor-elf/gui-for-ffmpeg/menu" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/migration" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/setting" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/theme" "go.etcd.io/bbolt" "golang.org/x/text/language" "os" @@ -26,24 +27,26 @@ func init() { appMetadata := &fyne.AppMetadata{ ID: "net.kor-elf.projects.gui-for-ffmpeg", Name: "GUI for FFmpeg", - Version: "0.8.0", + Version: "0.9.0", Icon: iconResource, } localizerService, err := kernel.NewLocalizer("languages", language.Russian) if err != nil { kernel.PanicErrorLang(err, appMetadata) + return } - ffPathUtilities = &kernel.FFPathUtilities{FFmpeg: "", FFprobe: ""} + ffPathUtilities = &kernel.FFPathUtilities{FFmpeg: "", FFprobe: "", FFplay: ""} convertorService := kernel.NewService(ffPathUtilities) + ffplayService := kernel.NewFFplay(ffPathUtilities) queue := kernel.NewQueueList() application = kernel.NewApp( appMetadata, localizerService, queue, - kernel.NewQueueLayoutObject(queue, localizerService), + ffplayService, convertorService, ) } @@ -92,16 +95,33 @@ func main() { } ffPathUtilities.FFprobe = pathFFprobe + pathFFplay, err := convertorRepository.GetPathFfplay() + if err != nil && errors.Is(err, dberror.ErrRecordNotFound) == false { + errorView.PanicError(err) + application.GetWindow().ShowAndRun() + return + } + ffPathUtilities.FFplay = pathFFplay + application.RunConvertor() defer application.AfterClosing() localizerView := localizer.NewView(application) convertorView := convertor.NewView(application) - convertorHandler := handler.NewConvertorHandler(application, convertorView, errorView, convertorRepository, settingDirectoryForSaving) + itemsToConvertService := kernel.NewItemsToConvert( + application.GetWindow().GetLayout().GetRightTabs().GetAddedFilesContainer(), + application.GetFFplayService(), + application.GetLocalizerService(), + ) + convertorHandler := handler.NewConvertorHandler(application, convertorView, errorView, convertorRepository, settingDirectoryForSaving, itemsToConvertService) + + themeRepository := theme.NewRepository(settingRepository) + themeService := theme.NewTheme(application, themeRepository) localizerRepository := localizer.NewRepository(settingRepository) menuView := menu.NewView(application) - mainMenu := handler.NewMenuHandler(application, convertorHandler, menuView, localizerView, localizerRepository) + menuSettingView := menu.NewViewSetting(application, themeService) + mainMenu := handler.NewMenuHandler(application, convertorHandler, menuView, menuSettingView, localizerView, localizerRepository, themeService) mainHandler := handler.NewMainHandler(application, convertorHandler, mainMenu, localizerRepository) mainHandler.Start() diff --git a/menu/view.go b/menu/view.go index 1d4fde2..324b012 100644 --- a/menu/view.go +++ b/menu/view.go @@ -4,6 +4,7 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" "github.com/nicksnyder/go-i18n/v2/i18n" @@ -12,8 +13,9 @@ import ( ) type ViewContract interface { - About(ffmpegVersion string, ffprobeVersion string) + About(ffmpegVersion string, ffprobeVersion string, ffplayVersion string) Gratitude() + HelpFFplay() } type View struct { @@ -60,7 +62,7 @@ func (v View) Gratitude() { view.Show() } -func (v View) About(ffmpegVersion string, ffprobeVersion string) { +func (v View) About(ffmpegVersion string, ffprobeVersion string, ffplayVersion string) { view := v.app.GetAppFyne().NewWindow(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "about", })) @@ -134,6 +136,7 @@ func (v View) About(ffmpegVersion string, ffprobeVersion string) { )), v.getAboutFfmpeg(ffmpegVersion), v.getAboutFfprobe(ffprobeVersion), + v.getAboutFfplay(ffplayVersion), widget.NewCard(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "AlsoUsedProgram", }), "", v.getOther()), @@ -143,6 +146,151 @@ func (v View) About(ffmpegVersion string, ffprobeVersion string) { view.Show() } +func (v View) HelpFFplay() { + view := v.app.GetAppFyne().NewWindow(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "helpFFplay", + })) + view.Resize(fyne.Size{Width: 800, Height: 550}) + view.SetFixedSize(true) + + data := [][]string{ + []string{ + v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "helpFFplayKeys", + }), + v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "helpFFplayDescription", + }), + }, + []string{ + "Q, ESC", + v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "helpFFplayQuit", + }), + }, + []string{ + "F, " + v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "helpFFplayDoubleClickLeftMouseButton", + }), + v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "helpFFplayToggleFullScreen", + }), + }, + []string{ + "P, " + + v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "helpFFplayKeySpace", + }), + v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "helpFFplayPause", + }), + }, + []string{ + "M", + v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "helpFFplayToggleMute", + }), + }, + []string{ + "9, /", + v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "helpFFplayDecreaseVolume", + }), + }, + []string{ + "0, *", + v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "helpFFplayIncreaseVolume", + }), + }, + []string{ + v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "helpFFplayKeyLeft", + }), + v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "helpFFplaySeekBackward10Seconds", + }), + }, + []string{ + v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "helpFFplayKeyRight", + }), + v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "helpFFplaySeekForward10Seconds", + }), + }, + []string{ + v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "helpFFplayKeyDown", + }), + v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "helpFFplaySeekBackward1Minute", + }), + }, + []string{ + v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "helpFFplayKeyUp", + }), + v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "helpFFplaySeekBForward1Minute", + }), + }, + []string{ + "Page Down", + v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "helpFFplaySeekBackward10Minutes", + }), + }, + []string{ + "Page Up", + v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "helpFFplaySeekBForward10Minutes", + }), + }, + []string{ + "S, " + v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "helpFFplayKeyHoldS", + }), + v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "helpFFplayActivateFrameStepMode", + }), + }, + []string{ + "W", + v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "helpFFplayCycleVideoFiltersOrShowModes", + }), + }, + } + + list := widget.NewTable( + func() (int, int) { + return len(data), len(data[0]) + }, + func() fyne.CanvasObject { + return widget.NewLabel("") + }, + func(i widget.TableCellID, o fyne.CanvasObject) { + if i.Row == 0 { + o.(*widget.Label).TextStyle.Bold = true + o.(*widget.Label).SizeName = theme.SizeNameSubHeadingText + } + if i.Col == 0 { + o.(*widget.Label).TextStyle.Bold = true + } + o.(*widget.Label).SetText(data[i.Row][i.Col]) + }) + list.SetRowHeight(0, 40) + list.SetColumnWidth(0, 200) + list.SetColumnWidth(1, 585) + list.SetRowHeight(2, 55) + view.SetContent( + container.NewScroll(list), + ) + view.CenterOnScreen() + view.Show() +} + func (v View) getCopyright() *widget.RichText { return widget.NewRichTextFromMarkdown("Copyright (c) 2024 **[Leonid Nikitin (kor-elf)](https://git.kor-elf.net/kor-elf/)**.") } @@ -207,6 +355,36 @@ func (v View) getAboutFfprobe(version string) *fyne.Container { ) } +func (v View) getAboutFfplay(version string) *fyne.Container { + programmName := canvas.NewText(" FFplay", colornames.Darkgreen) + programmName.TextStyle = fyne.TextStyle{Bold: true} + programmName.TextSize = 20 + + programmLink := widget.NewHyperlink(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "programmLink", + }), &url.URL{ + Scheme: "https", + Host: "ffmpeg.org", + Path: "ffplay.html", + }) + + licenseLink := widget.NewHyperlink(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "licenseLink", + }), &url.URL{ + Scheme: "https", + Host: "ffmpeg.org", + Path: "legal.html", + }) + + return container.NewVBox( + programmName, + widget.NewLabel(version), + widget.NewRichTextFromMarkdown("**FFmpeg** is a trademark of **[Fabrice Bellard](http://bellard.org/)**, originator of the **[FFmpeg](https://ffmpeg.org/about.html)** project."), + widget.NewRichTextFromMarkdown("This software uses libraries from the **FFmpeg** project under the **[LGPLv2.1](http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html)**."), + container.NewHBox(programmLink, licenseLink), + ) +} + func (v View) getOther() *fyne.Container { return container.NewVBox( canvas.NewLine(colornames.Darkgreen), diff --git a/menu/view_setting.go b/menu/view_setting.go new file mode 100644 index 0000000..045662e --- /dev/null +++ b/menu/view_setting.go @@ -0,0 +1,112 @@ +package menu + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/widget" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/theme" + "github.com/nicksnyder/go-i18n/v2/i18n" + "image/color" +) + +type ViewSettingContract interface { + Main( + save func(*SettingForm) error, + cancel func(), + ) +} + +type SettingForm struct { + Language kernel.Lang + ThemeInfo theme.ThemeInfoContract +} + +type ViewSetting struct { + app kernel.AppContract + themeService theme.ThemeContract +} + +func NewViewSetting(app kernel.AppContract, themeService theme.ThemeContract) *ViewSetting { + return &ViewSetting{ + app: app, + themeService: themeService, + } +} + +func (v ViewSetting) Main(save func(*SettingForm) error, cancel func()) { + errorMessage := canvas.NewText("", color.RGBA{R: 255, G: 0, B: 0, A: 255}) + errorMessage.TextSize = 16 + errorMessage.TextStyle = fyne.TextStyle{Bold: true} + + viewSettingForm := &SettingForm{ + Language: v.app.GetLocalizerService().GetCurrentLanguage().Lang, + ThemeInfo: v.themeService.GetCurrentThemeInfo(), + } + + languageItems := []string{} + langByTitle := map[string]kernel.Lang{} + for _, language := range v.app.GetLocalizerService().GetLanguages() { + languageItems = append(languageItems, language.Title) + langByTitle[language.Title] = language + } + selectLanguage := widget.NewSelect(languageItems, func(s string) { + if lang, ok := langByTitle[s]; ok { + viewSettingForm.Language = lang + } + }) + selectLanguage.Selected = v.app.GetLocalizerService().GetCurrentLanguage().Lang.Title + + themeItems := []string{} + themeByTitle := map[string]theme.ThemeInfoContract{} + for _, themeInfo := range v.themeService.List() { + themeItems = append(themeItems, themeInfo.GetTitle()) + themeByTitle[themeInfo.GetTitle()] = themeInfo + } + selectTheme := widget.NewSelect(themeItems, func(s string) { + if themeInfo, ok := themeByTitle[s]; ok { + viewSettingForm.ThemeInfo = themeInfo + } + }) + selectTheme.Selected = v.themeService.GetCurrentThemeInfo().GetTitle() + + form := &widget.Form{ + Items: []*widget.FormItem{ + { + Text: v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "menuSettingsLanguage", + }), + Widget: selectLanguage, + }, + { + Text: v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "menuSettingsTheme", + }), + Widget: selectTheme, + }, + { + Widget: errorMessage, + }, + }, + SubmitText: v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "save", + }), + OnSubmit: func() { + err := save(viewSettingForm) + if err != nil { + errorMessage.Text = err.Error() + } + }, + } + if cancel != nil { + form.OnCancel = cancel + form.CancelText = v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "cancel", + }) + } + + messageHead := v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ + MessageID: "settings", + }) + v.app.GetWindow().SetContent(widget.NewCard(messageHead, "", form)) +} diff --git a/theme/repository.go b/theme/repository.go new file mode 100644 index 0000000..91b2351 --- /dev/null +++ b/theme/repository.go @@ -0,0 +1,28 @@ +package theme + +import "git.kor-elf.net/kor-elf/gui-for-ffmpeg/setting" + +type RepositoryContract interface { + GetCode() string + Save(code string) (setting.Setting, error) +} + +type Repository struct { + settingRepository setting.RepositoryContract +} + +func NewRepository(settingRepository setting.RepositoryContract) *Repository { + return &Repository{settingRepository: settingRepository} +} + +func (r Repository) GetCode() string { + name, err := r.settingRepository.GetValue("theme") + if err != nil { + return "default" + } + return name +} + +func (r Repository) Save(code string) (setting.Setting, error) { + return r.settingRepository.CreateOrUpdate("theme", code) +} diff --git a/theme/theme.go b/theme/theme.go new file mode 100644 index 0000000..ba0dfa0 --- /dev/null +++ b/theme/theme.go @@ -0,0 +1,158 @@ +package theme + +import ( + "fyne.io/fyne/v2" + fyneTheme "fyne.io/fyne/v2/theme" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" + "github.com/nicksnyder/go-i18n/v2/i18n" + "image/color" +) + +type ThemeContract interface { + List() map[string]ThemeInfoContract + GetCurrentThemeInfo() ThemeInfoContract + SetCurrentTheme(themeInfo ThemeInfoContract) error +} + +type theme struct { + app kernel.AppContract + repository RepositoryContract + list map[string]ThemeInfoContract +} + +func NewTheme(app kernel.AppContract, repository RepositoryContract) ThemeContract { + theme := &theme{ + app: app, + repository: repository, + list: getThemes(app.GetLocalizerService()), + } + + theme.init() + + return theme +} + +func (t theme) init() { + themeInfo := t.GetCurrentThemeInfo() + if themeInfo.GetName() == "default" { + t.app.GetAppFyne().Settings().SetTheme(fyneTheme.DefaultTheme()) + return + } + t.app.GetAppFyne().Settings().SetTheme(&forcedVariant{theme: fyneTheme.DefaultTheme(), variant: themeInfo.GetVariant()}) +} + +func (t theme) GetCurrentThemeInfo() ThemeInfoContract { + themes := t.List() + if themeInfo, ok := themes[t.repository.GetCode()]; ok { + return themeInfo + } + + return themes["default"] +} + +func (t theme) List() map[string]ThemeInfoContract { + return t.list +} + +func (t theme) SetCurrentTheme(themeInfo ThemeInfoContract) error { + _, err := t.repository.Save(themeInfo.GetName()) + if err != nil { + return err + } + + if themeInfo.GetName() == "default" { + t.app.GetAppFyne().Settings().SetTheme(fyneTheme.DefaultTheme()) + return nil + } + t.app.GetAppFyne().Settings().SetTheme(&forcedVariant{theme: fyneTheme.DefaultTheme(), variant: themeInfo.GetVariant()}) + + return nil +} + +type ThemeInfoContract interface { + GetName() string + GetTitle() string + GetVariant() fyne.ThemeVariant +} + +type themeInfo struct { + name string + title string + variant fyne.ThemeVariant +} + +func (inf themeInfo) GetName() string { + return inf.name +} + +func (inf themeInfo) GetTitle() string { + return inf.title +} + +func (inf themeInfo) GetVariant() fyne.ThemeVariant { + return inf.variant +} + +func getThemes(localizer kernel.LocalizerContract) map[string]ThemeInfoContract { + themesNameDefault := &themeInfo{ + name: "default", + title: localizer.GetMessage(&i18n.LocalizeConfig{ + MessageID: "themesNameDefault", + }), + } + + themesNameLight := &themeInfo{ + name: "light", + title: localizer.GetMessage(&i18n.LocalizeConfig{ + MessageID: "themesNameLight", + }), + variant: fyneTheme.VariantLight, + } + + themesNameDark := &themeInfo{ + name: "dark", + title: localizer.GetMessage(&i18n.LocalizeConfig{ + MessageID: "themesNameDark", + }), + variant: fyneTheme.VariantDark, + } + + list := map[string]ThemeInfoContract{ + "default": themesNameDefault, + "light": themesNameLight, + "dark": themesNameDark, + } + + localizer.AddChangeCallback("themesNameDefault", func(text string) { + themesNameDefault.title = text + }) + localizer.AddChangeCallback("themesNameLight", func(text string) { + themesNameLight.title = text + }) + localizer.AddChangeCallback("themesNameDark", func(text string) { + themesNameDark.title = text + }) + + return list +} + +type forcedVariant struct { + theme fyne.Theme + variant fyne.ThemeVariant +} + +func (f *forcedVariant) Color(name fyne.ThemeColorName, _ fyne.ThemeVariant) color.Color { + return f.theme.Color(name, f.variant) +} + +func (f *forcedVariant) Font(style fyne.TextStyle) fyne.Resource { + return fyneTheme.DefaultTheme().Font(style) +} + +func (f *forcedVariant) Icon(name fyne.ThemeIconName) fyne.Resource { + return fyneTheme.DefaultTheme().Icon(name) +} + +func (f *forcedVariant) Size(name fyne.ThemeSizeName) float32 { + return fyneTheme.DefaultTheme().Size(name) +}