From d69767f5e97ea3428a185a2333bb9e793e20233f Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Fri, 30 May 2025 00:34:33 +0500 Subject: [PATCH 01/41] Remove bbolt database dependency Replaced bbolt-based database handling with Fyne built-in preferences for storing application settings. Deleted migration logic, database initialization, and error handling related to bbolt, simplifying the codebase and reducing external dependencies. --- FyneApp.toml | 5 +- README.md | 1 - convertor/repository.go | 24 ++++---- convertor/view/conversion.go | 7 +-- data/.gitignore | 2 - db/db.go | 7 --- error/view.go | 38 +----------- go.mod | 1 - go.sum | 4 -- handler/convertor.go | 18 +++--- handler/main.go | 6 +- handler/menu.go | 7 +-- images/screenshot-folder-structure.png | Bin 7544 -> 6871 bytes languages/active.en.toml | 8 --- languages/active.kk.toml | 8 --- languages/active.ru.toml | 2 - localizer/repository.go | 8 +-- main.go | 64 ++------------------ migration/migration.go | 12 ---- setting/directory_for_saving.go | 8 +-- setting/entity.go | 4 +- setting/repository.go | 79 ++++++------------------- theme/repository.go | 8 +-- theme/theme.go | 5 +- 24 files changed, 67 insertions(+), 259 deletions(-) delete mode 100644 data/.gitignore delete mode 100644 db/db.go delete mode 100644 migration/migration.go diff --git a/FyneApp.toml b/FyneApp.toml index 1fe1f80..b30925b 100644 --- a/FyneApp.toml +++ b/FyneApp.toml @@ -3,7 +3,4 @@ Name = "GUI for FFmpeg" ID = "net.kor-elf.projects.gui-for-ffmpeg" Version = "0.9.0" - Build = 4 - -[Migrations] - fyneDo = true \ No newline at end of file + Build = 11 diff --git a/README.md b/README.md index 8f59d1f..1d3cc54 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,6 @@ 7. Создаться папка **fyne-cross/bin** и там будет созданна папка с тем названием под которую Вы компилировали приложения (linux-amd64 или windows-amd64). 8. В папку **fyne-cross/bin/linux-amd64** или **fyne-cross/bin/windows-amd64** копируете: * icon.png - * data * languages * LICENSE * LICENSE-3RD-PARTY.txt diff --git a/convertor/repository.go b/convertor/repository.go index f751607..d052ff3 100644 --- a/convertor/repository.go +++ b/convertor/repository.go @@ -5,12 +5,12 @@ import ( ) type RepositoryContract interface { - GetPathFfmpeg() (string, error) - SavePathFfmpeg(code string) (setting.Setting, error) - GetPathFfprobe() (string, error) - SavePathFfprobe(code string) (setting.Setting, error) - GetPathFfplay() (string, error) - SavePathFfplay(code string) (setting.Setting, error) + GetPathFfmpeg() string + SavePathFfmpeg(code string) setting.Setting + GetPathFfprobe() string + SavePathFfprobe(code string) setting.Setting + GetPathFfplay() string + SavePathFfplay(code string) setting.Setting } type Repository struct { @@ -21,26 +21,26 @@ func NewRepository(settingRepository setting.RepositoryContract) *Repository { return &Repository{settingRepository: settingRepository} } -func (r Repository) GetPathFfmpeg() (string, error) { +func (r Repository) GetPathFfmpeg() string { return r.settingRepository.GetValue("ffmpeg") } -func (r Repository) SavePathFfmpeg(path string) (setting.Setting, error) { +func (r Repository) SavePathFfmpeg(path string) setting.Setting { return r.settingRepository.CreateOrUpdate("ffmpeg", path) } -func (r Repository) GetPathFfprobe() (string, error) { +func (r Repository) GetPathFfprobe() string { return r.settingRepository.GetValue("ffprobe") } -func (r Repository) SavePathFfprobe(path string) (setting.Setting, error) { +func (r Repository) SavePathFfprobe(path string) setting.Setting { return r.settingRepository.CreateOrUpdate("ffprobe", path) } -func (r Repository) GetPathFfplay() (string, error) { +func (r Repository) GetPathFfplay() string { return r.settingRepository.GetValue("ffplay") } -func (r Repository) SavePathFfplay(path string) (setting.Setting, error) { +func (r Repository) SavePathFfplay(path string) setting.Setting { return r.settingRepository.CreateOrUpdate("ffplay", path) } diff --git a/convertor/view/conversion.go b/convertor/view/conversion.go index 947a37c..7959bd9 100644 --- a/convertor/view/conversion.go +++ b/convertor/view/conversion.go @@ -353,7 +353,7 @@ func newDirectoryForSaving(app kernel.AppContract, settingDirectoryForSaving set locationURI, err = storage.ListerForURI(r) if err == nil { - _, _ = settingDirectoryForSaving.SaveDirectoryForSaving(locationURI.Path()) + _ = settingDirectoryForSaving.SaveDirectoryForSaving(locationURI.Path()) } }, locationURI) @@ -363,10 +363,7 @@ func newDirectoryForSaving(app kernel.AppContract, settingDirectoryForSaving set } func getDirectoryForSaving(settingDirectoryForSaving setting.DirectoryForSavingContract) (fyne.ListableURI, error) { - path, err := settingDirectoryForSaving.GetDirectoryForSaving() - if err != nil { - return nil, err - } + path := settingDirectoryForSaving.GetDirectoryForSaving() if len(path) > 0 { path = "file://" + path diff --git a/data/.gitignore b/data/.gitignore deleted file mode 100644 index c96a04f..0000000 --- a/data/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/db/db.go b/db/db.go deleted file mode 100644 index ce2ddf9..0000000 --- a/db/db.go +++ /dev/null @@ -1,7 +0,0 @@ -package db - -import "errors" - -var ( - ErrRecordNotFound = errors.New("record not found") -) diff --git a/error/view.go b/error/view.go index 2d68856..50182e6 100644 --- a/error/view.go +++ b/error/view.go @@ -1,14 +1,12 @@ 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 { @@ -37,17 +35,10 @@ func (v View) PanicError(err error) { 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(messagetText), + widget.NewLabel(err.Error()), ), nil, nil, @@ -57,30 +48,3 @@ 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", - }) - messageHead := v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "error", - }) - - v.app.GetWindow().SetContent(container.NewBorder( - container.NewVBox( - widget.NewLabel(messageHead), - widget.NewLabel(message), - ), - nil, - nil, - nil, - localizer.LanguageSelectionForm(v.app.GetLocalizerService(), func(lang kernel.Lang) { - v.PanicErrorWriteDirectoryData() - }), - )) -} diff --git a/go.mod b/go.mod index 721120e..1bbe1ff 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,6 @@ require ( github.com/BurntSushi/toml v1.5.0 github.com/nicksnyder/go-i18n/v2 v2.6.0 github.com/ulikunitz/xz v0.5.12 - go.etcd.io/bbolt v1.4.0 golang.org/x/image v0.27.0 golang.org/x/text v0.25.0 ) diff --git a/go.sum b/go.sum index 9db999c..2efe54d 100644 --- a/go.sum +++ b/go.sum @@ -67,14 +67,10 @@ github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/yuin/goldmark v1.7.11 h1:ZCxLyDMtz0nT2HFfsYG8WZ47Trip2+JyLysKcMYE5bo= github.com/yuin/goldmark v1.7.11/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= -go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= -go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w= golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= diff --git a/handler/convertor.go b/handler/convertor.go index a0cd29b..8643d35 100644 --- a/handler/convertor.go +++ b/handler/convertor.go @@ -61,9 +61,9 @@ func (h ConvertorHandler) MainConvertor() { } func (h ConvertorHandler) FfPathSelection() { - ffmpeg, _ := h.convertorRepository.GetPathFfmpeg() - ffprobe, _ := h.convertorRepository.GetPathFfprobe() - ffplay, _ := h.convertorRepository.GetPathFfplay() + ffmpeg := h.convertorRepository.GetPathFfmpeg() + ffprobe := h.convertorRepository.GetPathFfprobe() + ffplay := h.convertorRepository.GetPathFfplay() h.convertorView.SelectFFPath(ffmpeg, ffprobe, ffplay, h.saveSettingFFPath, h.MainConvertor, h.downloadFFmpeg) } @@ -122,9 +122,9 @@ func (h ConvertorHandler) checkingFFPathUtilities() bool { if ffplayChecking == false { continue } - _, _ = h.convertorRepository.SavePathFfmpeg(item.FFmpeg) - _, _ = h.convertorRepository.SavePathFfprobe(item.FFprobe) - _, _ = h.convertorRepository.SavePathFfplay(item.FFplay) + _ = h.convertorRepository.SavePathFfmpeg(item.FFmpeg) + _ = h.convertorRepository.SavePathFfprobe(item.FFprobe) + _ = h.convertorRepository.SavePathFfplay(item.FFplay) return true } @@ -156,9 +156,9 @@ func (h ConvertorHandler) saveSettingFFPath(ffmpegPath string, ffprobePath strin return errors.New(errorText) } - _, _ = h.convertorRepository.SavePathFfmpeg(ffmpegPath) - _, _ = h.convertorRepository.SavePathFfprobe(ffprobePath) - _, _ = h.convertorRepository.SavePathFfplay(ffplayPath) + _ = h.convertorRepository.SavePathFfmpeg(ffmpegPath) + _ = h.convertorRepository.SavePathFfprobe(ffprobePath) + _ = h.convertorRepository.SavePathFfplay(ffplayPath) h.MainConvertor() diff --git a/handler/main.go b/handler/main.go index 0e97c6f..e8a669b 100644 --- a/handler/main.go +++ b/handler/main.go @@ -28,9 +28,9 @@ func NewMainHandler( } func (h MainHandler) Start() { - language, err := h.localizerRepository.GetCode() - if err != nil { - err = h.app.GetLocalizerService().SetCurrentLanguageByCode(lang.SystemLocale().LanguageString()) + language := h.localizerRepository.GetCode() + if len(language) == 0 { + err := h.app.GetLocalizerService().SetCurrentLanguageByCode(lang.SystemLocale().LanguageString()) if err != nil { h.menuHandler.LanguageSelection() return diff --git a/handler/menu.go b/handler/menu.go index fa32d8b..7bc8b87 100644 --- a/handler/menu.go +++ b/handler/menu.go @@ -143,7 +143,7 @@ func (h MenuHandler) openAbout() { func (h MenuHandler) LanguageSelection() { h.localizerView.LanguageSelection(func(lang kernel.Lang) { - _, _ = h.localizerRepository.Save(lang.Code) + _ = h.localizerRepository.Save(lang.Code) h.convertorHandler.MainConvertor() }) } @@ -154,10 +154,7 @@ func (h MenuHandler) settingsSelection() { if err != nil { return err } - _, err = h.localizerRepository.Save(setting.Language.Code) - if err != nil { - return err - } + _ = h.localizerRepository.Save(setting.Language.Code) err = h.themeService.SetCurrentTheme(setting.ThemeInfo) if err != nil { diff --git a/images/screenshot-folder-structure.png b/images/screenshot-folder-structure.png index 19b2a3f82e9d0cf9f703136e7340762f5c41fb49..1e29071ac4a6242718a6b0381ecbbd6502454028 100644 GIT binary patch literal 6871 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 NP*Kp3uaYwh`F|wlnpXe- literal 7544 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 jSn+= Date: Sun, 1 Jun 2025 15:20:33 +0500 Subject: [PATCH 02/41] Refactor localization system and migrate to Fyne's built-in support Replaced the `i18n` and `toml` dependencies with Fyne's built-in language system for localization management. Updated the `Localizer` implementation to handle translations using JSON files and embed functionality. Simplified language selection and persisted settings via Fyne's preferences API. --- README.md | 12 - convertor/view.go | 5 +- convertor/view/conversion.go | 59 +- convertor/view/form_items/h264_nvenc/view.go | 5 +- convertor/view/form_items/libx264/view.go | 7 +- convertor/view/form_items/libx265/view.go | 7 +- convertor/view_setting.go | 33 +- ...ew_setting_button_download_ffmpeg_linux.go | 13 +- ..._setting_button_download_ffmpeg_windows.go | 13 +- error/view.go | 17 +- go.mod | 6 +- handler/convertor.go | 13 +- handler/convertor_linux.go | 17 +- handler/convertor_windows.go | 17 +- handler/main.go | 28 +- handler/menu.go | 76 +-- images/screenshot-folder-structure.png | Bin 6871 -> 5891 bytes kernel/app.go | 7 +- kernel/error.go | 5 +- kernel/items_to_convert.go | 23 +- kernel/layout.go | 7 +- kernel/localizer.go | 246 ++++---- kernel/progressbar.go | 17 +- kernel/right_tabs.go | 5 +- kernel/translations/app.en.json | 143 +++++ kernel/translations/app.kk.json | 143 +++++ kernel/translations/app.ru.json | 143 +++++ kernel/translations/base.en.json | 45 ++ kernel/translations/base.kk.json | 45 ++ kernel/translations/base.ru.json | 45 ++ languages/.gitignore | 1 - languages/active.en.toml | 563 ------------------ languages/active.kk.toml | 563 ------------------ languages/active.ru.toml | 141 ----- localizer/repository.go | 26 - localizer/view.go | 15 +- main.go | 13 +- menu/view.go | 218 +++---- menu/view_setting.go | 25 +- theme/theme.go | 19 +- 40 files changed, 917 insertions(+), 1869 deletions(-) create mode 100644 kernel/translations/app.en.json create mode 100644 kernel/translations/app.kk.json create mode 100644 kernel/translations/app.ru.json create mode 100644 kernel/translations/base.en.json create mode 100644 kernel/translations/base.kk.json create mode 100644 kernel/translations/base.ru.json delete mode 100644 languages/.gitignore delete mode 100644 languages/active.en.toml delete mode 100644 languages/active.kk.toml delete mode 100644 languages/active.ru.toml delete mode 100644 localizer/repository.go diff --git a/README.md b/README.md index 1d3cc54..38031b4 100644 --- a/README.md +++ b/README.md @@ -26,19 +26,7 @@ 7. Создаться папка **fyne-cross/bin** и там будет созданна папка с тем названием под которую Вы компилировали приложения (linux-amd64 или windows-amd64). 8. В папку **fyne-cross/bin/linux-amd64** или **fyne-cross/bin/windows-amd64** копируете: * icon.png - * languages * LICENSE * LICENSE-3RD-PARTY.txt

Структура должна получиться такая:

- -## Работа с переводами: -1. go install -v github.com/nicksnyder/go-i18n/v2/goi18n@latest -3. Создаём файл languages/translate.\*.toml -4. goi18n merge -sourceLanguage ru -outdir languages languages/active.\*.toml languages/translate.\*.toml -5. В файлах **languages/translate.\*.toml** переводим текст на нужный язык -6. goi18n merge -sourceLanguage ru -outdir languages languages/active.\*.toml languages/translate.\*.toml - -___где * подставляем нужный язык___ - -Более подробно можно почитать тут: https://github.com/nicksnyder/go-i18n \ No newline at end of file diff --git a/convertor/view.go b/convertor/view.go index 58e6a3f..e3dcb14 100644 --- a/convertor/view.go +++ b/convertor/view.go @@ -7,7 +7,6 @@ import ( "fyne.io/fyne/v2/widget" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/convertor/view" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" - "github.com/nicksnyder/go-i18n/v2/i18n" "image/color" ) @@ -45,9 +44,7 @@ func NewView(app kernel.AppContract) *View { } func (v View) Main(formConversion view.ConversionContract) { - converterVideoFilesTitle := v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "converterVideoFilesTitle", - }) + converterVideoFilesTitle := v.app.GetLocalizerService().GetMessage("converterVideoFilesTitle") v.app.GetWindow().SetContent(widget.NewCard(converterVideoFilesTitle, "", container.NewVScroll(formConversion.GetContent()))) formConversion.AfterViewContent() } diff --git a/convertor/view/conversion.go b/convertor/view/conversion.go index 7959bd9..1ceeafa 100644 --- a/convertor/view/conversion.go +++ b/convertor/view/conversion.go @@ -12,7 +12,6 @@ import ( "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel/encoder" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/setting" - "github.com/nicksnyder/go-i18n/v2/i18n" "image/color" "os" "path/filepath" @@ -54,14 +53,14 @@ func NewConversion(app kernel.AppContract, formats encoder.ConvertorFormatsContr items := []*widget.FormItem{ { - Text: app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{MessageID: "fileForConversionTitle"}), + Text: app.GetLocalizerService().GetMessage("fileForConversionTitle"), Widget: fileForConversion.button, }, { Widget: container.NewHScroll(fileForConversion.message), }, { - Text: app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{MessageID: "buttonForSelectedDirTitle"}), + Text: app.GetLocalizerService().GetMessage("buttonForSelectedDirTitle"), Widget: directoryForSaving.button, }, { @@ -74,11 +73,11 @@ func NewConversion(app kernel.AppContract, formats encoder.ConvertorFormatsContr Widget: selectEncoder.SelectFileType, }, { - Text: app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{MessageID: "selectFormat"}), + Text: app.GetLocalizerService().GetMessage("selectFormat"), Widget: selectEncoder.SelectFormat, }, { - Text: app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{MessageID: "selectEncoder"}), + Text: app.GetLocalizerService().GetMessage("selectEncoder"), Widget: selectEncoder.SelectEncoder, }, } @@ -141,30 +140,22 @@ func (c Conversion) selectFileForConversion(err error) { func (c Conversion) submit() { if len(c.itemsToConvertService.GetItems()) == 0 { - showConversionMessage(c.conversionMessage, errors.New(c.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "errorNoFilesAddedForConversion", - }))) + showConversionMessage(c.conversionMessage, errors.New(c.app.GetLocalizerService().GetMessage("errorNoFilesAddedForConversion"))) c.enableFormConversion() return } if len(c.directoryForSaving.path) == 0 { - showConversionMessage(c.conversionMessage, errors.New(c.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "errorSelectedFolderSave", - }))) + showConversionMessage(c.conversionMessage, errors.New(c.app.GetLocalizerService().GetMessage("errorSelectedFolderSave"))) c.enableFormConversion() return } if len(c.selectEncoder.SelectFormat.Selected) == 0 { - showConversionMessage(c.conversionMessage, errors.New(c.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "errorSelectedFormat", - }))) + showConversionMessage(c.conversionMessage, errors.New(c.app.GetLocalizerService().GetMessage("errorSelectedFormat"))) return } if c.selectEncoder.Encoder == nil { - showConversionMessage(c.conversionMessage, errors.New(c.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "errorSelectedEncoder", - }))) + showConversionMessage(c.conversionMessage, errors.New(c.app.GetLocalizerService().GetMessage("errorSelectedEncoder"))) return } c.conversionMessage.Text = "" @@ -208,13 +199,9 @@ func newFileForConversion(app kernel.AppContract, itemsToConvertService kernel.I changeCallbacks: map[int]func(err error){}, } - buttonTitle := app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "choose", - }) + "\n" + app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "or", - }) + "\n" + app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "dragAndDropFiles", - }) + buttonTitle := app.GetLocalizerService().GetMessage("choose") + "\n" + + app.GetLocalizerService().GetMessage("or") + "\n" + + app.GetLocalizerService().GetMessage("dragAndDropFiles") var locationURI fyne.ListableURI @@ -281,9 +268,7 @@ func newFileForConversion(app kernel.AppContract, itemsToConvertService kernel.I } app.GetWindow().GetLayout().GetRightTabs().SelectAddedFilesTab() if isError { - fileForConversion.message.Text = app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "errorDragAndDropFile", - }) + fileForConversion.message.Text = app.GetLocalizerService().GetMessage("errorDragAndDropFile") setStringErrorStyle(fileForConversion.message) fileForConversion.eventSelectFile(errors.New(fileForConversion.message.Text)) } else { @@ -322,9 +307,7 @@ func newDirectoryForSaving(app kernel.AppContract, settingDirectoryForSaving set directoryForSaving.message.TextSize = 16 directoryForSaving.message.TextStyle = fyne.TextStyle{Bold: true} - buttonTitle := app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "choose", - }) + buttonTitle := app.GetLocalizerService().GetMessage("choose") var locationURI fyne.ListableURI @@ -386,9 +369,7 @@ func newOverwriteOutputFiles(app kernel.AppContract) *overwriteOutputFiles { overwriteOutputFiles := &overwriteOutputFiles{ isChecked: false, } - checkboxOverwriteOutputFilesTitle := app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "checkboxOverwriteOutputFilesTitle", - }) + checkboxOverwriteOutputFilesTitle := app.GetLocalizerService().GetMessage("checkboxOverwriteOutputFilesTitle") overwriteOutputFiles.checkbox = widget.NewCheck(checkboxOverwriteOutputFilesTitle, func(b bool) { overwriteOutputFiles.isChecked = b }) @@ -437,7 +418,7 @@ func newSelectEncoder(app kernel.AppContract, formats encoder.ConvertorFormatsCo encoders = map[int]encoder2.EncoderDataContract{} for _, e := range format.GetEncoders() { encoders[len(encoders)] = e - encoderOptions = append(encoderOptions, app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{MessageID: "encoder_" + e.GetTitle()})) + encoderOptions = append(encoderOptions, app.GetLocalizerService().GetMessage("encoder_"+e.GetTitle())) } selectEncoder.SelectEncoder.SetOptions(encoderOptions) selectEncoder.SelectEncoder.SetSelectedIndex(0) @@ -448,9 +429,9 @@ func newSelectEncoder(app kernel.AppContract, formats encoder.ConvertorFormatsCo fileTypeOptions = append(fileTypeOptions, fileType.Name()) } - encoderGroupVideo := app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{MessageID: "encoderGroupVideo"}) - encoderGroupAudio := app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{MessageID: "encoderGroupAudio"}) - encoderGroupImage := app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{MessageID: "encoderGroupImage"}) + encoderGroupVideo := app.GetLocalizerService().GetMessage("encoderGroupVideo") + encoderGroupAudio := app.GetLocalizerService().GetMessage("encoderGroupAudio") + encoderGroupImage := app.GetLocalizerService().GetMessage("encoderGroupImage") encoderGroup := map[string]string{ encoderGroupVideo: "video", encoderGroupAudio: "audio", @@ -517,9 +498,7 @@ type form struct { func newForm(app kernel.AppContract, items []*widget.FormItem) *form { f := widget.NewForm() - f.SubmitText = app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "converterVideoFilesSubmitTitle", - }) + f.SubmitText = app.GetLocalizerService().GetMessage("converterVideoFilesSubmitTitle") f.Items = items return &form{ diff --git a/convertor/view/form_items/h264_nvenc/view.go b/convertor/view/form_items/h264_nvenc/view.go index 81b3c15..2f92333 100644 --- a/convertor/view/form_items/h264_nvenc/view.go +++ b/convertor/view/form_items/h264_nvenc/view.go @@ -6,7 +6,6 @@ import ( "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/h264_nvenc" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" - "github.com/nicksnyder/go-i18n/v2/i18n" ) func View(encoder encoder.EncoderContract, app kernel.AppContract) []*widget.FormItem { @@ -45,7 +44,7 @@ func presetParameter(encoder encoder.EncoderContract, app kernel.AppContract) [] elementSelect.SetSelected(presetDefault) elementSelect.Hide() - checkboxTitle := app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{MessageID: "parameterCheckbox"}) + checkboxTitle := app.GetLocalizerService().GetMessage("parameterCheckbox") elementCheckbox := widget.NewCheck(checkboxTitle, func(b bool) { if b == true { parameter.SetEnable() @@ -58,7 +57,7 @@ func presetParameter(encoder encoder.EncoderContract, app kernel.AppContract) [] return []*widget.FormItem{ { - Text: app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{MessageID: "formPreset"}), + Text: app.GetLocalizerService().GetMessage("formPreset"), Widget: container.NewVBox(elementCheckbox, elementSelect), }, } diff --git a/convertor/view/form_items/libx264/view.go b/convertor/view/form_items/libx264/view.go index f552c66..dd8ae34 100644 --- a/convertor/view/form_items/libx264/view.go +++ b/convertor/view/form_items/libx264/view.go @@ -6,7 +6,6 @@ import ( "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/libx264" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" - "github.com/nicksnyder/go-i18n/v2/i18n" ) func View(encoder encoder.EncoderContract, app kernel.AppContract) []*widget.FormItem { @@ -28,7 +27,7 @@ func presetParameter(encoder encoder.EncoderContract, app kernel.AppContract) [] presetDefault := "" for _, name := range libx264.Presets { - title := app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{MessageID: "preset_" + name}) + title := app.GetLocalizerService().GetMessage("preset_" + name) presetsForSelect = append(presetsForSelect, title) presets[title] = name if name == parameter.Get() { @@ -45,7 +44,7 @@ func presetParameter(encoder encoder.EncoderContract, app kernel.AppContract) [] elementSelect.SetSelected(presetDefault) elementSelect.Hide() - checkboxTitle := app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{MessageID: "parameterCheckbox"}) + checkboxTitle := app.GetLocalizerService().GetMessage("parameterCheckbox") elementCheckbox := widget.NewCheck(checkboxTitle, func(b bool) { if b == true { parameter.SetEnable() @@ -58,7 +57,7 @@ func presetParameter(encoder encoder.EncoderContract, app kernel.AppContract) [] return []*widget.FormItem{ { - Text: app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{MessageID: "formPreset"}), + Text: app.GetLocalizerService().GetMessage("formPreset"), Widget: container.NewVBox(elementCheckbox, elementSelect), }, } diff --git a/convertor/view/form_items/libx265/view.go b/convertor/view/form_items/libx265/view.go index 434defd..ae71d4d 100644 --- a/convertor/view/form_items/libx265/view.go +++ b/convertor/view/form_items/libx265/view.go @@ -6,7 +6,6 @@ import ( "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/libx265" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" - "github.com/nicksnyder/go-i18n/v2/i18n" ) func View(encoder encoder.EncoderContract, app kernel.AppContract) []*widget.FormItem { @@ -28,7 +27,7 @@ func presetParameter(encoder encoder.EncoderContract, app kernel.AppContract) [] presetDefault := "" for _, name := range libx265.Presets { - title := app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{MessageID: "preset_" + name}) + title := app.GetLocalizerService().GetMessage("preset_" + name) presetsForSelect = append(presetsForSelect, title) presets[title] = name if name == parameter.Get() { @@ -45,7 +44,7 @@ func presetParameter(encoder encoder.EncoderContract, app kernel.AppContract) [] elementSelect.SetSelected(presetDefault) elementSelect.Hide() - checkboxTitle := app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{MessageID: "parameterCheckbox"}) + checkboxTitle := app.GetLocalizerService().GetMessage("parameterCheckbox") elementCheckbox := widget.NewCheck(checkboxTitle, func(b bool) { if b == true { parameter.SetEnable() @@ -58,7 +57,7 @@ func presetParameter(encoder encoder.EncoderContract, app kernel.AppContract) [] return []*widget.FormItem{ { - Text: app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{MessageID: "formPreset"}), + Text: app.GetLocalizerService().GetMessage("formPreset"), Widget: container.NewVBox(elementCheckbox, elementSelect), }, } diff --git a/convertor/view_setting.go b/convertor/view_setting.go index ca5e3f4..b18a070 100644 --- a/convertor/view_setting.go +++ b/convertor/view_setting.go @@ -6,7 +6,6 @@ import ( "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/storage" "fyne.io/fyne/v2/widget" - "github.com/nicksnyder/go-i18n/v2/i18n" "image/color" "net/url" "path/filepath" @@ -37,33 +36,25 @@ func (v View) SelectFFPath( form := &widget.Form{ Items: []*widget.FormItem{ { - Text: v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "titleDownloadLink", - }), + Text: v.app.GetLocalizerService().GetMessage("titleDownloadLink"), Widget: link, }, { - Text: v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "pathToFfmpeg", - }), + Text: v.app.GetLocalizerService().GetMessage("pathToFfmpeg"), Widget: buttonFFmpeg, }, { Widget: container.NewHScroll(buttonFFmpegMessage), }, { - Text: v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "pathToFfprobe", - }), + Text: v.app.GetLocalizerService().GetMessage("pathToFfprobe"), Widget: buttonFFprobe, }, { Widget: container.NewHScroll(buttonFFprobeMessage), }, { - Text: v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "pathToFfplay", - }), + Text: v.app.GetLocalizerService().GetMessage("pathToFfplay"), Widget: buttonFFplay, }, { @@ -73,9 +64,7 @@ func (v View) SelectFFPath( Widget: errorMessage, }, }, - SubmitText: v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "save", - }), + SubmitText: v.app.GetLocalizerService().GetMessage("save"), OnSubmit: func() { err := save(*ffmpegPath, *ffprobePath, *ffplayPath) if err != nil { @@ -85,13 +74,9 @@ func (v View) SelectFFPath( } if cancel != nil { form.OnCancel = cancel - form.CancelText = v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "cancel", - }) + form.CancelText = v.app.GetLocalizerService().GetMessage("cancel") } - selectFFPathTitle := v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "selectFFPathTitle", - }) + selectFFPathTitle := v.app.GetLocalizerService().GetMessage("selectFFPathTitle") if v.downloadFFmpeg.blockDownloadFFmpegContainer == nil { v.downloadFFmpeg.blockDownloadFFmpegContainer = v.blockDownloadFFmpeg(donwloadFFmpeg) @@ -110,9 +95,7 @@ func (v View) getButtonSelectFile(path string) (filePath *string, button *widget buttonMessage.TextSize = 16 buttonMessage.TextStyle = fyne.TextStyle{Bold: true} - buttonTitle := v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "choose", - }) + buttonTitle := v.app.GetLocalizerService().GetMessage("choose") var locationURI fyne.ListableURI if len(path) > 0 { diff --git a/convertor/view_setting_button_download_ffmpeg_linux.go b/convertor/view_setting_button_download_ffmpeg_linux.go index aa462d3..d08900c 100644 --- a/convertor/view_setting_button_download_ffmpeg_linux.go +++ b/convertor/view_setting_button_download_ffmpeg_linux.go @@ -8,7 +8,6 @@ import ( "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/widget" - "github.com/nicksnyder/go-i18n/v2/i18n" "golang.org/x/image/colornames" "image/color" ) @@ -29,9 +28,7 @@ func (v View) blockDownloadFFmpeg( var buttonDownloadFFmpeg *widget.Button - buttonDownloadFFmpeg = widget.NewButton(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "download", - }), func() { + buttonDownloadFFmpeg = widget.NewButton(v.app.GetLocalizerService().GetMessage("download"), func() { fyne.Do(func() { buttonDownloadFFmpeg.Disable() }) @@ -47,15 +44,11 @@ func (v View) blockDownloadFFmpeg( }) - downloadFFmpegFromSiteMessage := v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "downloadFFmpegFromSite", - }) + downloadFFmpegFromSiteMessage := v.app.GetLocalizerService().GetMessage("downloadFFmpegFromSite") return container.NewVBox( canvas.NewLine(colornames.Darkgreen), - widget.NewCard(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "buttonDownloadFFmpeg", - }), "", container.NewVBox( + widget.NewCard(v.app.GetLocalizerService().GetMessage("buttonDownloadFFmpeg"), "", container.NewVBox( widget.NewRichTextFromMarkdown( downloadFFmpegFromSiteMessage+" [https://github.com/BtbN/FFmpeg-Builds/releases](https://github.com/BtbN/FFmpeg-Builds/releases)", ), diff --git a/convertor/view_setting_button_download_ffmpeg_windows.go b/convertor/view_setting_button_download_ffmpeg_windows.go index 5b706b6..b71deef 100644 --- a/convertor/view_setting_button_download_ffmpeg_windows.go +++ b/convertor/view_setting_button_download_ffmpeg_windows.go @@ -8,7 +8,6 @@ import ( "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/widget" - "github.com/nicksnyder/go-i18n/v2/i18n" "golang.org/x/image/colornames" "image/color" ) @@ -29,9 +28,7 @@ func (v View) blockDownloadFFmpeg( var buttonDownloadFFmpeg *widget.Button - buttonDownloadFFmpeg = widget.NewButton(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "download", - }), func() { + buttonDownloadFFmpeg = widget.NewButton(v.app.GetLocalizerService().GetMessage("download"), func() { go func() { fyne.Do(func() { @@ -47,15 +44,11 @@ func (v View) blockDownloadFFmpeg( }() }) - downloadFFmpegFromSiteMessage := v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "downloadFFmpegFromSite", - }) + downloadFFmpegFromSiteMessage := v.app.GetLocalizerService().GetMessage("downloadFFmpegFromSite") return container.NewVBox( canvas.NewLine(colornames.Darkgreen), - widget.NewCard(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "buttonDownloadFFmpeg", - }), "", container.NewVBox( + widget.NewCard(v.app.GetLocalizerService().GetMessage("buttonDownloadFFmpeg"), "", container.NewVBox( widget.NewRichTextFromMarkdown( downloadFFmpegFromSiteMessage+" [https://github.com/BtbN/FFmpeg-Builds/releases](https://github.com/BtbN/FFmpeg-Builds/releases)", ), diff --git a/error/view.go b/error/view.go index 50182e6..f2becd4 100644 --- a/error/view.go +++ b/error/view.go @@ -2,11 +2,9 @@ package error import ( "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" ) type ViewContract interface { @@ -14,26 +12,17 @@ type ViewContract interface { } type View struct { - app kernel.AppContract - isSetLanguage bool + app kernel.AppContract } func NewView(app kernel.AppContract) *View { return &View{ - app: app, - isSetLanguage: true, + app: app, } } 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", - }) + messageHead := v.app.GetLocalizerService().GetMessage("error") v.app.GetWindow().SetContent(container.NewBorder( container.NewVBox( diff --git a/go.mod b/go.mod index 1bbe1ff..2c96e56 100644 --- a/go.mod +++ b/go.mod @@ -6,15 +6,13 @@ toolchain go1.23.9 require ( fyne.io/fyne/v2 v2.6.1 - github.com/BurntSushi/toml v1.5.0 - github.com/nicksnyder/go-i18n/v2 v2.6.0 github.com/ulikunitz/xz v0.5.12 golang.org/x/image v0.27.0 - golang.org/x/text v0.25.0 ) require ( fyne.io/systray v1.11.0 // indirect + github.com/BurntSushi/toml v1.5.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fredbi/uri v1.1.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect @@ -33,6 +31,7 @@ require ( github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect github.com/kr/text v0.2.0 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect + github.com/nicksnyder/go-i18n/v2 v2.6.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rymdport/portal v0.4.1 // indirect github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect @@ -41,5 +40,6 @@ require ( github.com/yuin/goldmark v1.7.11 // indirect golang.org/x/net v0.40.0 // indirect golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/handler/convertor.go b/handler/convertor.go index 8643d35..2bc81fe 100644 --- a/handler/convertor.go +++ b/handler/convertor.go @@ -8,7 +8,6 @@ import ( "git.kor-elf.net/kor-elf/gui-for-ffmpeg/helper" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/setting" - "github.com/nicksnyder/go-i18n/v2/i18n" ) type ConvertorHandlerContract interface { @@ -134,25 +133,19 @@ func (h ConvertorHandler) checkingFFPathUtilities() bool { 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{ - MessageID: "errorFFmpeg", - }) + errorText := h.app.GetLocalizerService().GetMessage("errorFFmpeg") return errors.New(errorText) } ffprobeChecking, _ := h.app.GetConvertorService().ChangeFFprobePath(ffprobePath) if ffprobeChecking == false { - errorText := h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "errorFFprobe", - }) + errorText := h.app.GetLocalizerService().GetMessage("errorFFprobe") return errors.New(errorText) } ffplayChecking, _ := h.app.GetConvertorService().ChangeFFplayPath(ffplayPath) if ffplayChecking == false { - errorText := h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "errorFFplay", - }) + errorText := h.app.GetLocalizerService().GetMessage("errorFFplay") return errors.New(errorText) } diff --git a/handler/convertor_linux.go b/handler/convertor_linux.go index c1a032a..1c87a3c 100644 --- a/handler/convertor_linux.go +++ b/handler/convertor_linux.go @@ -10,7 +10,6 @@ import ( "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/widget" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" - "github.com/nicksnyder/go-i18n/v2/i18n" "github.com/ulikunitz/xz" "io" "net/http" @@ -30,9 +29,7 @@ func (h ConvertorHandler) downloadFFmpeg(progressBar *widget.ProgressBar, progre return err } } - progressMessage.Text = h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "downloadRun", - }) + progressMessage.Text = h.app.GetLocalizerService().GetMessage("downloadRun") fyne.Do(func() { progressMessage.Refresh() }) @@ -41,9 +38,7 @@ func (h ConvertorHandler) downloadFFmpeg(progressBar *widget.ProgressBar, progre return err } - progressMessage.Text = h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "unzipRun", - }) + progressMessage.Text = h.app.GetLocalizerService().GetMessage("unzipRun") fyne.Do(func() { progressMessage.Refresh() }) @@ -53,9 +48,7 @@ func (h ConvertorHandler) downloadFFmpeg(progressBar *widget.ProgressBar, progre } _ = os.Remove("ffmpeg/ffmpeg.tar.xz") - progressMessage.Text = h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "testFF", - }) + progressMessage.Text = h.app.GetLocalizerService().GetMessage("testFF") fyne.Do(func() { progressMessage.Refresh() }) @@ -69,9 +62,7 @@ func (h ConvertorHandler) downloadFFmpeg(progressBar *widget.ProgressBar, progre return err } - progressMessage.Text = h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "completedQueue", - }) + progressMessage.Text = h.app.GetLocalizerService().GetMessage("completedQueue") fyne.Do(func() { progressMessage.Refresh() }) diff --git a/handler/convertor_windows.go b/handler/convertor_windows.go index fb08d87..206aa57 100644 --- a/handler/convertor_windows.go +++ b/handler/convertor_windows.go @@ -10,7 +10,6 @@ import ( "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/widget" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" - "github.com/nicksnyder/go-i18n/v2/i18n" "io" "net/http" "os" @@ -30,9 +29,7 @@ func (h ConvertorHandler) downloadFFmpeg(progressBar *widget.ProgressBar, progre return err } } - progressMessage.Text = h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "downloadRun", - }) + progressMessage.Text = h.app.GetLocalizerService().GetMessage("downloadRun") fyne.Do(func() { progressMessage.Refresh() }) @@ -41,9 +38,7 @@ func (h ConvertorHandler) downloadFFmpeg(progressBar *widget.ProgressBar, progre return err } - progressMessage.Text = h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "unzipRun", - }) + progressMessage.Text = h.app.GetLocalizerService().GetMessage("unzipRun") fyne.Do(func() { progressMessage.Refresh() }) @@ -53,9 +48,7 @@ func (h ConvertorHandler) downloadFFmpeg(progressBar *widget.ProgressBar, progre } _ = os.Remove("ffmpeg/ffmpeg.zip") - progressMessage.Text = h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "testFF", - }) + progressMessage.Text = h.app.GetLocalizerService().GetMessage("testFF") fyne.Do(func() { progressMessage.Refresh() }) @@ -68,9 +61,7 @@ func (h ConvertorHandler) downloadFFmpeg(progressBar *widget.ProgressBar, progre return err } - progressMessage.Text = h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "completedQueue", - }) + progressMessage.Text = h.app.GetLocalizerService().GetMessage("completedQueue") fyne.Do(func() { progressMessage.Refresh() }) diff --git a/handler/main.go b/handler/main.go index e8a669b..09d4fa8 100644 --- a/handler/main.go +++ b/handler/main.go @@ -1,42 +1,32 @@ 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" ) type MainHandler struct { - app kernel.AppContract - convertorHandler ConvertorHandlerContract - menuHandler MenuHandlerContract - localizerRepository localizer.RepositoryContract + app kernel.AppContract + convertorHandler ConvertorHandlerContract + menuHandler MenuHandlerContract } func NewMainHandler( app kernel.AppContract, convertorHandler ConvertorHandlerContract, menuHandler MenuHandlerContract, - localizerRepository localizer.RepositoryContract, ) *MainHandler { return &MainHandler{ - app: app, - convertorHandler: convertorHandler, - menuHandler: menuHandler, - localizerRepository: localizerRepository, + app: app, + convertorHandler: convertorHandler, + menuHandler: menuHandler, } } func (h MainHandler) Start() { - language := h.localizerRepository.GetCode() - if len(language) == 0 { - err := h.app.GetLocalizerService().SetCurrentLanguageByCode(lang.SystemLocale().LanguageString()) - if err != nil { - h.menuHandler.LanguageSelection() - return - } + if h.app.GetLocalizerService().IsStartWithLanguageSelection() { + h.menuHandler.LanguageSelection() + return } - _ = h.app.GetLocalizerService().SetCurrentLanguageByCode(language) h.convertorHandler.MainConvertor() } diff --git a/handler/menu.go b/handler/menu.go index 7bc8b87..932a0ed 100644 --- a/handler/menu.go +++ b/handler/menu.go @@ -6,7 +6,6 @@ import ( "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" ) type MenuHandlerContract interface { @@ -15,13 +14,12 @@ type MenuHandlerContract interface { } type MenuHandler struct { - app kernel.AppContract - convertorHandler ConvertorHandlerContract - menuView menu.ViewContract - menuViewSetting menu.ViewSettingContract - localizerView localizer.ViewContract - localizerRepository localizer.RepositoryContract - themeService theme.ThemeContract + app kernel.AppContract + convertorHandler ConvertorHandlerContract + menuView menu.ViewContract + menuViewSetting menu.ViewSettingContract + localizerView localizer.ViewContract + themeService theme.ThemeContract } func NewMenuHandler( @@ -30,17 +28,15 @@ func NewMenuHandler( 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, + app: app, + convertorHandler: convertorHandler, + menuView: menuView, + menuViewSetting: menuViewSetting, + localizerView: localizerView, + themeService: themeService, } } @@ -52,31 +48,23 @@ func (h MenuHandler) GetMainMenu() *fyne.MainMenu { } func (h MenuHandler) getMenuSettings() *fyne.Menu { - quit := fyne.NewMenuItem(h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "exit", - }), nil) + quit := fyne.NewMenuItem(h.app.GetLocalizerService().GetMessage("exit"), nil) quit.IsQuit = true h.app.GetLocalizerService().AddChangeCallback("exit", func(text string) { quit.Label = text }) - settingsSelection := fyne.NewMenuItem(h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "settings", - }), h.settingsSelection) + settingsSelection := fyne.NewMenuItem(h.app.GetLocalizerService().GetMessage("settings"), h.settingsSelection) h.app.GetLocalizerService().AddChangeCallback("settings", func(text string) { settingsSelection.Label = text }) - ffPathSelection := fyne.NewMenuItem(h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "changeFFPath", - }), h.convertorHandler.FfPathSelection) + ffPathSelection := fyne.NewMenuItem(h.app.GetLocalizerService().GetMessage("changeFFPath"), h.convertorHandler.FfPathSelection) h.app.GetLocalizerService().AddChangeCallback("changeFFPath", func(text string) { ffPathSelection.Label = text }) - settings := fyne.NewMenu(h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "settings", - }), settingsSelection, ffPathSelection, quit) + settings := fyne.NewMenu(h.app.GetLocalizerService().GetMessage("settings"), settingsSelection, ffPathSelection, quit) h.app.GetLocalizerService().AddChangeCallback("settings", func(text string) { settings.Label = text settings.Refresh() @@ -86,30 +74,22 @@ func (h MenuHandler) getMenuSettings() *fyne.Menu { } func (h MenuHandler) getMenuHelp() *fyne.Menu { - about := fyne.NewMenuItem(h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "about", - }), h.openAbout) + about := fyne.NewMenuItem(h.app.GetLocalizerService().GetMessage("about"), h.openAbout) h.app.GetLocalizerService().AddChangeCallback("about", func(text string) { about.Label = text }) - gratitude := fyne.NewMenuItem(h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "gratitude", - }), h.menuView.Gratitude) + gratitude := fyne.NewMenuItem(h.app.GetLocalizerService().GetMessage("gratitude"), 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) + helpFFplay := fyne.NewMenuItem(h.app.GetLocalizerService().GetMessage("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", - }), helpFFplay, about, gratitude) + help := fyne.NewMenu(h.app.GetLocalizerService().GetMessage("help"), helpFFplay, about, gratitude) h.app.GetLocalizerService().AddChangeCallback("help", func(text string) { help.Label = text help.Refresh() @@ -121,21 +101,15 @@ func (h MenuHandler) getMenuHelp() *fyne.Menu { func (h MenuHandler) openAbout() { ffmpeg, err := h.convertorHandler.GetFfmpegVersion() if err != nil { - ffmpeg = h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "errorFFmpegVersion", - }) + ffmpeg = h.app.GetLocalizerService().GetMessage("errorFFmpegVersion") } ffprobe, err := h.convertorHandler.GetFfprobeVersion() if err != nil { - ffprobe = h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "errorFFprobeVersion", - }) + ffprobe = h.app.GetLocalizerService().GetMessage("errorFFprobeVersion") } ffplay, err := h.convertorHandler.GetFfplayVersion() if err != nil { - ffplay = h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "errorFFplayVersion", - }) + ffplay = h.app.GetLocalizerService().GetMessage("errorFFplayVersion") } h.menuView.About(ffmpeg, ffprobe, ffplay) @@ -143,18 +117,16 @@ func (h MenuHandler) openAbout() { func (h MenuHandler) LanguageSelection() { h.localizerView.LanguageSelection(func(lang kernel.Lang) { - _ = h.localizerRepository.Save(lang.Code) h.convertorHandler.MainConvertor() }) } func (h MenuHandler) settingsSelection() { save := func(setting *menu.SettingForm) error { - err := h.app.GetLocalizerService().SetCurrentLanguage(setting.Language) + err := h.app.GetLocalizerService().SetCurrentLanguage(setting.Language, true) if err != nil { return err } - _ = h.localizerRepository.Save(setting.Language.Code) err = h.themeService.SetCurrentTheme(setting.ThemeInfo) if err != nil { diff --git a/images/screenshot-folder-structure.png b/images/screenshot-folder-structure.png index 1e29071ac4a6242718a6b0381ecbbd6502454028..58b4406fd5e0cefedcaa3999969c49cc8d4b901c 100644 GIT binary patch delta 5819 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 bBC!3?(!S1Q-4%H`qy{uq^;D{r?W6t&ISGkK delta 6807 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 Nl9pnn0=7l){{S=~osa+k diff --git a/kernel/app.go b/kernel/app.go index 3babb14..242ea8c 100644 --- a/kernel/app.go +++ b/kernel/app.go @@ -30,7 +30,6 @@ type App struct { func NewApp( metadata *fyne.AppMetadata, - localizerService LocalizerContract, queue QueueListContract, ffplayService FFplayContract, convertorService ConvertorContract, @@ -38,6 +37,12 @@ func NewApp( app.SetMetadata(*metadata) a := app.New() + localizerService, err := newLocalizer(a) + if err != nil { + panicErrorLang(a, err) + return nil + } + statusesText := GetBlockProgressbarStatusesText(localizerService) blockProgressbarService := NewBlockProgressbar(statusesText, ffplayService) rightTabsService := NewRightTabs(localizerService) diff --git a/kernel/error.go b/kernel/error.go index 7f03ea1..cd26e13 100644 --- a/kernel/error.go +++ b/kernel/error.go @@ -2,14 +2,11 @@ package kernel import ( "fyne.io/fyne/v2" - "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/widget" ) -func PanicErrorLang(err error, metadata *fyne.AppMetadata) { - app.SetMetadata(*metadata) - a := app.New() +func panicErrorLang(a fyne.App, err error) { window := a.NewWindow("GUI for FFmpeg") window.SetContent(container.NewVBox( widget.NewLabel("Произошла ошибка!"), diff --git a/kernel/items_to_convert.go b/kernel/items_to_convert.go index 0adbc3d..093540c 100644 --- a/kernel/items_to_convert.go +++ b/kernel/items_to_convert.go @@ -6,7 +6,6 @@ import ( "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 { @@ -35,21 +34,23 @@ func NewItemsToConvert(itemsContainer *fyne.Container, ffplayService FFplayContr 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 := widget.NewCheck( + localizerService.GetMessage("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 := widget.NewButton( + localizerService.GetMessage("clearAll"), + func() { + ItemsToConvert.clear() + }, + ) buttonClear.Importance = widget.DangerImportance localizerService.AddChangeCallback("clearAll", func(text string) { buttonClear.Text = text diff --git a/kernel/layout.go b/kernel/layout.go index 3d34179..7953967 100644 --- a/kernel/layout.go +++ b/kernel/layout.go @@ -6,7 +6,6 @@ import ( "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" - "github.com/nicksnyder/go-i18n/v2/i18n" "image/color" "strconv" "strings" @@ -77,7 +76,7 @@ type QueueLayoutItem struct { } func NewQueueLayoutObject(queue QueueListContract, localizerService LocalizerContract, ffplayService FFplayContract, rightTabsService RightTabsContract, blockProgressbar *fyne.Container) *QueueLayoutObject { - title := widget.NewLabel(localizerService.GetMessage(&i18n.LocalizeConfig{MessageID: "queue"})) + title := widget.NewLabel(localizerService.GetMessage("queue")) title.TextStyle.Bold = true localizerService.AddChangeCallback("queue", func(text string) { @@ -222,7 +221,7 @@ func (o QueueLayoutObject) getStatusColor(status StatusContract) color.Color { } func (o QueueLayoutObject) getStatusTitle(status StatusContract) string { - return o.localizerService.GetMessage(&i18n.LocalizeConfig{MessageID: status.Name() + "Queue"}) + return o.localizerService.GetMessage(status.Name() + "Queue") } type queueStatistics struct { @@ -441,7 +440,7 @@ func newQueueStatistics(messaigeID string, localizerService LocalizerContract) * count := int64(0) - title := localizerService.GetMessage(&i18n.LocalizeConfig{MessageID: messaigeID}) + title := localizerService.GetMessage(messaigeID) queueStatistics := &queueStatistics{ widget: checkbox, title: strings.ToLower(title), diff --git a/kernel/localizer.go b/kernel/localizer.go index 3823c7d..085fb69 100644 --- a/kernel/localizer.go +++ b/kernel/localizer.go @@ -1,21 +1,28 @@ package kernel import ( - "github.com/BurntSushi/toml" - "github.com/nicksnyder/go-i18n/v2/i18n" - "golang.org/x/text/cases" + "embed" + "encoding/json" + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/lang" "golang.org/x/text/language" - "golang.org/x/text/language/display" - "path/filepath" - "sort" ) +//go:embed translations +var translations embed.FS + +var supportedLanguages = map[string]Lang{ + "ru": {Code: "ru", Title: "Русский"}, + "kk": {Code: "kk", Title: "Қазақ Тілі"}, + "en": {Code: "en", Title: "English"}, +} + type LocalizerContract interface { + IsStartWithLanguageSelection() bool + GetMessage(key string, data ...any) string GetLanguages() []Lang - GetMessage(localizeConfig *i18n.LocalizeConfig) string - SetCurrentLanguage(lang Lang) error - SetCurrentLanguageByCode(code string) error - GetCurrentLanguage() *CurrentLanguage + GetCurrentLanguage() Lang + SetCurrentLanguage(selectLang Lang, isSaveSetting bool) error AddChangeCallback(messageID string, callback func(text string)) } @@ -24,135 +31,164 @@ type Lang struct { Title string } -type CurrentLanguage struct { - Lang Lang - localizer *i18n.Localizer - localizerDefault *i18n.Localizer -} - type changeCallback struct { messageID string callback func(text string) } type Localizer struct { - bundle *i18n.Bundle - languages []Lang - currentLanguage *CurrentLanguage - changeCallbacks map[int]*changeCallback + setting SettingLanguageContract + currentLang Lang + changeCallbacks map[int]*changeCallback + isStartWithLanguageSelection bool } -func NewLocalizer(directory string, languageDefault language.Tag) (*Localizer, error) { - bundle := i18n.NewBundle(languageDefault) - bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) +func newLocalizer(app fyne.App) (*Localizer, error) { + setting := newSettingLanguage(app) + currentLanguage, isLanguageNotSupported := setting.GetLang() - languages, err := initLanguages(directory, bundle) + localizer := &Localizer{ + setting: setting, + changeCallbacks: map[int]*changeCallback{}, + isStartWithLanguageSelection: isLanguageNotSupported, + } + + err := localizer.SetCurrentLanguage(currentLanguage, false) if err != nil { return nil, err } - localizerDefault := i18n.NewLocalizer(bundle, languageDefault.String()) - - return &Localizer{ - bundle: bundle, - languages: languages, - currentLanguage: &CurrentLanguage{ - Lang: Lang{ - Code: languageDefault.String(), - Title: cases.Title(languageDefault).String(display.Self.Name(languageDefault)), - }, - localizer: localizerDefault, - localizerDefault: localizerDefault, - }, - changeCallbacks: map[int]*changeCallback{}, - }, nil + return localizer, nil } -func initLanguages(directory string, bundle *i18n.Bundle) ([]Lang, error) { - var languages []Lang +func (l *Localizer) IsStartWithLanguageSelection() bool { + return l.isStartWithLanguageSelection +} - files, err := filepath.Glob(directory + "/active.*.toml") +func (l *Localizer) GetMessage(key string, data ...any) string { + return lang.L(key, data...) +} + +func (l *Localizer) GetLanguages() []Lang { + return getLanguages() +} + +func (l *Localizer) GetCurrentLanguage() Lang { + return l.currentLang +} + +func (l *Localizer) SetCurrentLanguage(selectLang Lang, isSaveSetting bool) error { + l.currentLang = selectLang + + translationsData, err := l.getTranslations(selectLang) if err != nil { - return nil, err - } - for _, file := range files { - lang, err := bundle.LoadMessageFile(file) - if err != nil { - return nil, err - } - title := cases.Title(lang.Tag).String(display.Self.Name(lang.Tag)) - languages = append(languages, Lang{Code: lang.Tag.String(), Title: title}) + return err } - sort.Sort(languagesSort(languages)) - - return languages, nil -} - -func (l Localizer) GetLanguages() []Lang { - return l.languages -} - -func (l Localizer) GetMessage(localizeConfig *i18n.LocalizeConfig) string { - message, err := l.GetCurrentLanguage().localizer.Localize(localizeConfig) + name := lang.SystemLocale().LanguageString() + err = lang.AddTranslations(fyne.NewStaticResource(name+".json", translationsData)) if err != nil { - message, err = l.GetCurrentLanguage().localizerDefault.Localize(localizeConfig) - if err != nil { - return err.Error() - } + return err + } + + if isSaveSetting { + l.setting.SetLang(selectLang) } - return message -} -func (l Localizer) SetCurrentLanguage(lang Lang) error { - l.currentLanguage.Lang = lang - l.currentLanguage.localizer = i18n.NewLocalizer(l.bundle, lang.Code) l.eventSetCurrentLanguage() return nil } -func (l Localizer) SetCurrentLanguageByCode(code string) error { - lang, err := language.Parse(code) - if err != nil { - return err - } - title := cases.Title(lang).String(display.Self.Name(lang)) - return l.SetCurrentLanguage(Lang{Code: lang.String(), Title: title}) -} - -func (l Localizer) GetCurrentLanguage() *CurrentLanguage { - return l.currentLanguage -} - -func (l Localizer) AddChangeCallback(messageID string, callback func(text string)) { +func (l *Localizer) AddChangeCallback(messageID string, callback func(text string)) { l.changeCallbacks[len(l.changeCallbacks)] = &changeCallback{messageID: messageID, callback: callback} } -func (l Localizer) eventSetCurrentLanguage() { +func (l *Localizer) eventSetCurrentLanguage() { for _, changeCallback := range l.changeCallbacks { - text := l.GetMessage(&i18n.LocalizeConfig{MessageID: changeCallback.messageID}) + text := l.GetMessage(changeCallback.messageID) changeCallback.callback(text) } } -type languagesSort []Lang - -func (l languagesSort) Len() int { return len(l) } -func (l languagesSort) Swap(i, j int) { l[i], l[j] = l[j], l[i] } -func (l languagesSort) Less(i, j int) bool { - return languagePriority(l[i]) < languagePriority(l[j]) -} -func languagePriority(l Lang) int { - priority := 0 - - switch l.Code { - case "ru": - priority = -3 - case "kk": - priority = -2 - case "en": - priority = -1 +func (l *Localizer) getTranslations(language Lang) ([]byte, error) { + baseJson, err := translations.ReadFile("translations/base." + language.Code + ".json") + if err != nil { + return nil, err + } + appJson, err := translations.ReadFile("translations/app." + language.Code + ".json") + if err != nil { + return nil, err } - return priority + return l.mergeTranslations(baseJson, appJson) +} + +func (l *Localizer) mergeTranslations(baseJson []byte, appJson []byte) ([]byte, error) { + base := map[string]interface{}{} + custom := map[string]interface{}{} + err := json.Unmarshal(baseJson, &base) + if err != nil { + return nil, err + } + err = json.Unmarshal(appJson, &custom) + if err != nil { + return nil, err + } + + for k, v := range custom { + base[k] = v + } + return json.Marshal(base) +} + +func getLanguages() []Lang { + items := []Lang{} + for _, item := range supportedLanguages { + items = append(items, item) + } + return items +} + +type SettingLanguageContract interface { + GetLang() (currentLang Lang, isLanguageNotSupported bool) + SetLang(language Lang) +} + +type SettingLanguage struct { + app fyne.App +} + +func newSettingLanguage(app fyne.App) *SettingLanguage { + return &SettingLanguage{ + app: app, + } +} + +func (s *SettingLanguage) GetLang() (currentLang Lang, isLanguageNotSupported bool) { + languageCode := s.app.Preferences().String("language") + currentLang = supportedLanguages["ru"] + + if languageCode == "" { + languageTag, err := language.Parse(lang.SystemLocale().LanguageString()) + if err != nil { + return currentLang, true + } + base, _ := languageTag.Base() + languageCode = base.String() + } + + if findLang, ok := findSupportedLanguage(languageCode); ok { + return findLang, false + } + + return currentLang, true +} + +func (s *SettingLanguage) SetLang(language Lang) { + s.app.Preferences().SetString("language", language.Code) +} + +func findSupportedLanguage(code string) (Lang, bool) { + lang, ok := supportedLanguages[code] + return lang, ok } diff --git a/kernel/progressbar.go b/kernel/progressbar.go index 48b8485..95d4ce5 100644 --- a/kernel/progressbar.go +++ b/kernel/progressbar.go @@ -8,7 +8,6 @@ import ( "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" @@ -209,9 +208,7 @@ func (p Progress) Run(stdOut io.ReadCloser, stdErr io.ReadCloser) error { if isProcessCompleted == false { if len(errorText) == 0 { - errorText = p.localizerService.GetMessage(&i18n.LocalizeConfig{ - MessageID: "errorConverter", - }) + errorText = p.localizerService.GetMessage("errorConverter") } return errors.New(errorText) } @@ -227,15 +224,9 @@ type BlockProgressbarStatusesText struct { 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", - }), + inProgress: localizerService.GetMessage("inProgressQueue"), + completed: localizerService.GetMessage("completedQueue"), + error: localizerService.GetMessage("errorQueue"), } localizerService.AddChangeCallback("inProgressQueue", func(text string) { diff --git a/kernel/right_tabs.go b/kernel/right_tabs.go index 16f8de1..9b41880 100644 --- a/kernel/right_tabs.go +++ b/kernel/right_tabs.go @@ -3,7 +3,6 @@ package kernel import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" - "github.com/nicksnyder/go-i18n/v2/i18n" ) type RightTabsContract interface { @@ -26,13 +25,13 @@ type RightTabs struct { func NewRightTabs(localizerService LocalizerContract) *RightTabs { addedFilesContainer := container.NewVBox() - addedFilesTab := container.NewTabItem(localizerService.GetMessage(&i18n.LocalizeConfig{MessageID: "addedFilesTitle"}), addedFilesContainer) + addedFilesTab := container.NewTabItem(localizerService.GetMessage("addedFilesTitle"), addedFilesContainer) localizerService.AddChangeCallback("addedFilesTitle", func(text string) { addedFilesTab.Text = text }) fileQueueContainer := container.NewVBox() - fileQueueTab := container.NewTabItem(localizerService.GetMessage(&i18n.LocalizeConfig{MessageID: "fileQueueTitle"}), fileQueueContainer) + fileQueueTab := container.NewTabItem(localizerService.GetMessage("fileQueueTitle"), fileQueueContainer) localizerService.AddChangeCallback("fileQueueTitle", func(text string) { fileQueueTab.Text = text }) diff --git a/kernel/translations/app.en.json b/kernel/translations/app.en.json new file mode 100644 index 0000000..9919f8d --- /dev/null +++ b/kernel/translations/app.en.json @@ -0,0 +1,143 @@ +{ + "AlsoUsedProgram": "The program also uses:", + "about": "About", + "aboutText": "A simple interface for the FFmpeg console utility. \nBut I am not the author of the FFmpeg utility itself.", + "addedFilesTitle": "Added files", + "autoClearAfterAddingToQueue": "Auto-clear after adding to queue", + "buttonDownloadFFmpeg": "Download FFmpeg automatically", + "buttonForSelectedDirTitle": "Save to folder:", + "cancel": "Cancel", + "changeFFPath": "FFmpeg, FFprobe and FFplay", + "changeLanguage": "Change language", + "checkboxOverwriteOutputFilesTitle": "Allow file to be overwritten", + "choose": "choose", + "clearAll": "Clear List", + "completedQueue": "Completed", + "converterVideoFilesSubmitTitle": "Convert", + "converterVideoFilesTitle": "Video, audio and picture converter", + "download": "Download", + "downloadFFmpegFromSite": "Will be downloaded from the site:", + "downloadRun": "Downloading...", + "dragAndDropFiles": "drag and drop files", + "encoderGroupAudio": "Audio", + "encoderGroupImage": "Images", + "encoderGroupVideo": "Video", + "encoder_apng": "APNG image", + "encoder_bmp": "BMP image", + "encoder_flv": "FLV", + "encoder_gif": "GIF image", + "encoder_h264_nvenc": "H.264 with NVIDIA support", + "encoder_libmp3lame": "libmp3lame MP3 (MPEG audio layer 3)", + "encoder_libshine": "libshine MP3 (MPEG audio layer 3)", + "encoder_libtwolame": "libtwolame MP2 (MPEG audio layer 2)", + "encoder_libvpx": "libvpx VP8 (codec vp8)", + "encoder_libvpx-vp9": "libvpx VP9 (codec vp9)", + "encoder_libwebp": "libwebp WebP image", + "encoder_libwebp_anim": "libwebp_anim WebP image", + "encoder_libx264": "H.264 libx264", + "encoder_libx265": "H.265 libx265", + "encoder_libxvid": "libxvidcore MPEG-4 part 2", + "encoder_mjpeg": "MJPEG (Motion JPEG)", + "encoder_mp2": "MP2 (MPEG audio layer 2)", + "encoder_mp2fixed": "MP2 fixed point (MPEG audio layer 2)", + "encoder_mpeg1video": "MPEG-1", + "encoder_mpeg2video": "MPEG-2", + "encoder_mpeg4": "MPEG-4 part 2", + "encoder_msmpeg4": "MPEG-4 part 2 Microsoft variant version 3", + "encoder_msmpeg4v2": "MPEG-4 part 2 Microsoft variant version 2", + "encoder_msvideo1": "Microsoft Video-1", + "encoder_png": "PNG image", + "encoder_qtrle": "QuickTime Animation (RLE) video", + "encoder_sgi": "SGI image", + "encoder_tiff": "TIFF image", + "encoder_wmav1": "Windows Media Audio 1", + "encoder_wmav2": "Windows Media Audio 2", + "encoder_wmv1": "Windows Media Video 7", + "encoder_wmv2": "Windows Media Video 8", + "encoder_xbm": "XBM (X BitMap) image", + "error": "An error has occurred!", + "errorConverter": "Couldn't convert video", + "errorDragAndDropFile": "Not all files were added", + "errorFFmpeg": "this is not FFmpeg", + "errorFFmpegVersion": "Could not determine FFmpeg version", + "errorFFplay": "this is not FFplay", + "errorFFplayVersion": "Could not determine FFplay version", + "errorFFprobe": "this is not FFprobe", + "errorFFprobeVersion": "Failed to determine FFprobe version", + "errorNoFilesAddedForConversion": "There are no files to convert", + "errorQueue": "Error", + "errorSelectedEncoder": "Converter not selected", + "errorSelectedFolderSave": "No save folder selected!", + "errorSelectedFormat": "File extension not selected", + "exit": "Exit", + "ffmpegLGPL": "This software uses libraries from the **FFmpeg** project under the **[LGPLv2.1](http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html)**.", + "ffmpegTrademark": "**FFmpeg** is a trademark of **[Fabrice Bellard](http://bellard.org/)**, originator of the **[FFmpeg](https://ffmpeg.org/about.html)** project.", + "fileForConversionTitle": "File:", + "fileQueueTitle": "Queue", + "formPreset": "Preset", + "gratitude": "Gratitude", + "gratitudeText": "I sincerely thank you for your invaluable\n\r and timely assistance:", + "help": "Help", + "helpFFplay": "FFplay Player Keys", + "helpFFplayActivateFrameStepMode": "Activate frame-by-frame mode.", + "helpFFplayCycleVideoFiltersOrShowModes": "A cycle of video filters or display modes.", + "helpFFplayDecreaseVolume": "Decrease the volume.", + "helpFFplayDescription": "Description", + "helpFFplayDoubleClickLeftMouseButton": "double click\nleft mouse button", + "helpFFplayIncreaseVolume": "Increase the volume.", + "helpFFplayKeyDown": "down", + "helpFFplayKeyHoldS": "hold S", + "helpFFplayKeyLeft": "left", + "helpFFplayKeyRight": "right", + "helpFFplayKeySpace": "SPACE", + "helpFFplayKeyUp": "up", + "helpFFplayKeys": "Keys", + "helpFFplayPause": "Pause or continue playing.", + "helpFFplayQuit": "Close the player.", + "helpFFplaySeekBForward10Minutes": "Fast forward 10 minutes.", + "helpFFplaySeekBForward1Minute": "Fast forward 1 minute.", + "helpFFplaySeekBackward10Minutes": "Rewind 10 minutes.", + "helpFFplaySeekBackward10Seconds": "Rewind 10 seconds.", + "helpFFplaySeekBackward1Minute": "Rewind 1 minute.", + "helpFFplaySeekForward10Seconds": "Fast forward 10 seconds.", + "helpFFplayToggleFullScreen": "Switch to full screen or exit full screen.", + "helpFFplayToggleMute": "Mute or unmute.", + "inProgressQueue": "In Progress", + "languageSelectionFormHead": "Switch language", + "languageSelectionHead": "Choose language", + "licenseLink": "License information", + "licenseLinkOther": "Licenses from other products used in the program", + "menuSettingsLanguage": "Language", + "menuSettingsTheme": "Theme", + "or": "or", + "parameterCheckbox": "Enable option", + "pathToFfmpeg": "Path to FFmpeg:", + "pathToFfplay": "Path to FFplay:", + "pathToFfprobe": "Path to FFprobe:", + "preset_fast": "fast (slower than \"faster\", but the file will weigh less)", + "preset_faster": "faster (slower than \"veryfast\", but the file will weigh less)", + "preset_medium": "medium (slower than \"fast\", but the file will weigh less)", + "preset_placebo": "placebo (not recommended)", + "preset_slow": "slow (slower than \"medium\", but the file will weigh less)", + "preset_slower": "slower (slower than \"slow\", but the file will weigh less)", + "preset_superfast": "superfast (slower than \"ultrafast\", but the file will weigh less)", + "preset_ultrafast": "ultrafast (fast, but the file will weigh a lot)", + "preset_veryfast": "veryfast (slower than \"superfast\", but the file will weigh less)", + "preset_veryslow": "veryslow (slower than \"slower\", but the file will weigh less)", + "programmLink": "Project website", + "programmVersion": "**Program version:** {{.Version}}", + "queue": "Queue", + "save": "Save", + "selectEncoder": "Encoder:", + "selectFFPathTitle": "Specify the path to FFmpeg and FFprobe", + "selectFormat": "File extension:", + "settings": "Settings", + "testFF": "Checking FFmpeg for serviceability...", + "themesNameDark": "Dark", + "themesNameDefault": "Default", + "themesNameLight": "Light", + "titleDownloadLink": "You can download it from here", + "total": "Total", + "unzipRun": "Unpacked...", + "waitingQueue": "Waiting" +} \ No newline at end of file diff --git a/kernel/translations/app.kk.json b/kernel/translations/app.kk.json new file mode 100644 index 0000000..6fb1b81 --- /dev/null +++ b/kernel/translations/app.kk.json @@ -0,0 +1,143 @@ +{ + "AlsoUsedProgram": "Бағдарлама сонымен қатар пайдаланады:", + "about": "Бағдарлама туралы", + "aboutText": "FFmpeg консоль утилитасы үшін қарапайым интерфейс. \nБірақ мен FFmpeg утилитасының авторы емеспін.", + "addedFilesTitle": "Қосылған файлдар", + "autoClearAfterAddingToQueue": "Кезекке қосқаннан кейін тазалаңыз", + "buttonDownloadFFmpeg": "FFmpeg автоматты түрде жүктеп алыңыз", + "buttonForSelectedDirTitle": "Қалтаға сақтаңыз:", + "cancel": "Болдырмау", + "changeFFPath": "FFmpeg, FFprobe және FFplay", + "changeLanguage": "Тілді өзгерту", + "checkboxOverwriteOutputFilesTitle": "Файлды қайта жазуға рұқсат беріңіз", + "choose": "таңдау", + "clearAll": "Тізімді өшіру", + "completedQueue": "Дайын", + "converterVideoFilesSubmitTitle": "Файлды түрлендіру", + "converterVideoFilesTitle": "Бейне, аудио және суретті түрлендіргіш", + "download": "Жүктеп алу", + "downloadFFmpegFromSite": "Сайттан жүктеледі:", + "downloadRun": "Жүктеп алынуда...", + "dragAndDropFiles": "файлдарды сүйреп апарыңыз", + "encoderGroupAudio": "Аудио", + "encoderGroupImage": "Суреттер", + "encoderGroupVideo": "Бейне", + "encoder_apng": "APNG image", + "encoder_bmp": "BMP image", + "encoder_flv": "FLV", + "encoder_gif": "GIF image", + "encoder_h264_nvenc": "NVIDIA қолдауымен H.264", + "encoder_libmp3lame": "libmp3lame MP3 (MPEG audio layer 3)", + "encoder_libshine": "libshine MP3 (MPEG audio layer 3)", + "encoder_libtwolame": "libtwolame MP2 (MPEG audio layer 2)", + "encoder_libvpx": "libvpx VP8 (codec vp8)", + "encoder_libvpx-vp9": "libvpx VP9 (codec vp9)", + "encoder_libwebp": "libwebp WebP image", + "encoder_libwebp_anim": "libwebp_anim WebP image", + "encoder_libx264": "H.264 libx264", + "encoder_libx265": "H.265 libx265", + "encoder_libxvid": "libxvidcore MPEG-4 part 2", + "encoder_mjpeg": "MJPEG (Motion JPEG)", + "encoder_mp2": "MP2 (MPEG audio layer 2)", + "encoder_mp2fixed": "MP2 fixed point (MPEG audio layer 2)", + "encoder_mpeg1video": "MPEG-1", + "encoder_mpeg2video": "MPEG-2", + "encoder_mpeg4": "MPEG-4 part 2", + "encoder_msmpeg4": "MPEG-4 part 2 Microsoft variant version 3", + "encoder_msmpeg4v2": "MPEG-4 part 2 Microsoft variant version 2", + "encoder_msvideo1": "Microsoft Video-1", + "encoder_png": "PNG image", + "encoder_qtrle": "QuickTime Animation (RLE) video", + "encoder_sgi": "SGI image", + "encoder_tiff": "TIFF image", + "encoder_wmav1": "Windows Media Audio 1", + "encoder_wmav2": "Windows Media Audio 2", + "encoder_wmv1": "Windows Media Video 7", + "encoder_wmv2": "Windows Media Video 8", + "encoder_xbm": "XBM (X BitMap) image", + "error": "Қате орын алды!", + "errorConverter": "Бейнені түрлендіру мүмкін болмады", + "errorDragAndDropFile": "Барлық файлдар қосылмаған", + "errorFFmpeg": "бұл FFmpeg емес", + "errorFFmpegVersion": "FFmpeg нұсқасын анықтау мүмкін болмады", + "errorFFplay": "бұл FFplay емес", + "errorFFplayVersion": "FFplay нұсқасын анықтау мүмкін болмады", + "errorFFprobe": "бұл FFprobe емес", + "errorFFprobeVersion": "FFprobe нұсқасын анықтау мүмкін болмады", + "errorNoFilesAddedForConversion": "Түрлендіруге арналған файлдар жоқ", + "errorQueue": "Қате", + "errorSelectedEncoder": "Түрлендіргіш таңдалмаған", + "errorSelectedFolderSave": "Сақтау қалтасы таңдалмаған!", + "errorSelectedFormat": "Файл кеңейтімі таңдалмаған", + "exit": "Шығу", + "ffmpegLGPL": "Бұл бағдарламалық құрал **[LGPLv2.1](http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html)** астында **FFmpeg** жобасының кітапханаларын пайдаланады.", + "ffmpegTrademark": "FFmpeg — **[FFmpeg](https://ffmpeg.org/about.html)** жобасын жасаушы **[Fabrice Bellard](http://bellard.org/)** сауда белгісі.", + "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» қарағанда баяуырақ, бірақ файлдың салмағы аз болады)", + "preset_medium": "medium («fast» қарағанда баяуырақ, бірақ файлдың салмағы аз болады)", + "preset_placebo": "placebo (ұсынылмайды)", + "preset_slow": "slow («medium» қарағанда баяуырақ, бірақ файлдың салмағы аз болады)", + "preset_slower": "slower («slow» қарағанда баяуырақ, бірақ файлдың салмағы аз болады)", + "preset_superfast": "superfast («ultrafast» қарағанда баяуырақ, бірақ файлдың салмағы аз болады)", + "preset_ultrafast": "ultrafast (жылдам, бірақ файлдың салмағы көп болады)", + "preset_veryfast": "veryfast («superfast» қарағанда баяуырақ, бірақ файлдың салмағы аз болады)", + "preset_veryslow": "veryslow («slower» қарағанда баяуырақ, бірақ файлдың салмағы аз болады)", + "programmLink": "Жобаның веб-сайты", + "programmVersion": "**Бағдарлама нұсқасы:** {{.Version}}", + "queue": "Кезек", + "save": "Сақтау", + "selectEncoder": "Кодировщик:", + "selectFFPathTitle": "FFmpeg және FFprobe жолын көрсетіңіз", + "selectFormat": "Файл кеңейтімі:", + "settings": "Параметрлер", + "testFF": "FFmpeg функционалдығы тексерілуде...", + "themesNameDark": "Қараңғы тақырып", + "themesNameDefault": "Әдепкі бойынша", + "themesNameLight": "Жеңіл тақырып", + "titleDownloadLink": "Сіз оны осы жерден жүктей аласыз", + "total": "Барлығы", + "unzipRun": "Орамнан шығарылуда...", + "waitingQueue": "Күту" +} \ No newline at end of file diff --git a/kernel/translations/app.ru.json b/kernel/translations/app.ru.json new file mode 100644 index 0000000..20a5da8 --- /dev/null +++ b/kernel/translations/app.ru.json @@ -0,0 +1,143 @@ +{ + "AlsoUsedProgram": "Также в программе используется:", + "about": "О программе", + "aboutText": "Простенький интерфейс для консольной утилиты FFmpeg. \nНо я не являюсь автором самой утилиты FFmpeg.", + "addedFilesTitle": "Добавленные файлы", + "autoClearAfterAddingToQueue": "Очищать после добавления в очередь", + "buttonDownloadFFmpeg": "Скачать автоматически FFmpeg", + "buttonForSelectedDirTitle": "Сохранить в папку:", + "cancel": "Отмена", + "changeFFPath": "FFmpeg, FFprobe и FFplay", + "changeLanguage": "Поменять язык", + "checkboxOverwriteOutputFilesTitle": "Разрешить перезаписать файл", + "choose": "выбрать", + "clearAll": "Очистить список", + "completedQueue": "Готово", + "converterVideoFilesSubmitTitle": "Конвертировать", + "converterVideoFilesTitle": "Конвертер видео, аудио и картинок", + "download": "Скачать", + "downloadFFmpegFromSite": "Будет скачано с сайта:", + "downloadRun": "Скачивается...", + "dragAndDropFiles": "перетащить файлы", + "encoderGroupAudio": "Аудио", + "encoderGroupImage": "Картинки", + "encoderGroupVideo": "Видео", + "encoder_apng": "APNG image", + "encoder_bmp": "BMP image", + "encoder_flv": "FLV", + "encoder_gif": "GIF image", + "encoder_h264_nvenc": "H.264 с поддержкой NVIDIA", + "encoder_libmp3lame": "libmp3lame MP3 (MPEG audio layer 3)", + "encoder_libshine": "libshine MP3 (MPEG audio layer 3)", + "encoder_libtwolame": "libtwolame MP2 (MPEG audio layer 2)", + "encoder_libvpx": "libvpx VP8 (codec vp8)", + "encoder_libvpx-vp9": "libvpx VP9 (codec vp9)", + "encoder_libwebp": "libwebp WebP image", + "encoder_libwebp_anim": "libwebp_anim WebP image", + "encoder_libx264": "H.264 libx264", + "encoder_libx265": "H.265 libx265", + "encoder_libxvid": "libxvidcore MPEG-4 part 2", + "encoder_mjpeg": "MJPEG (Motion JPEG)", + "encoder_mp2": "MP2 (MPEG audio layer 2)", + "encoder_mp2fixed": "MP2 fixed point (MPEG audio layer 2)", + "encoder_mpeg1video": "MPEG-1", + "encoder_mpeg2video": "MPEG-2", + "encoder_mpeg4": "MPEG-4 part 2", + "encoder_msmpeg4": "MPEG-4 part 2 Microsoft variant version 3", + "encoder_msmpeg4v2": "MPEG-4 part 2 Microsoft variant version 2", + "encoder_msvideo1": "Microsoft Video-1", + "encoder_png": "PNG image", + "encoder_qtrle": "QuickTime Animation (RLE) video", + "encoder_sgi": "SGI image", + "encoder_tiff": "TIFF image", + "encoder_wmav1": "Windows Media Audio 1", + "encoder_wmav2": "Windows Media Audio 2", + "encoder_wmv1": "Windows Media Video 7", + "encoder_wmv2": "Windows Media Video 8", + "encoder_xbm": "XBM (X BitMap) image", + "error": "Произошла ошибка!", + "errorConverter": "не смогли отконвертировать видео", + "errorDragAndDropFile": "Не все файлы добавились", + "errorFFmpeg": "это не FFmpeg", + "errorFFmpegVersion": "Не смогли определить версию FFmpeg", + "errorFFplay": "это не FFplay", + "errorFFplayVersion": "Не смогли определить версию FFplay", + "errorFFprobe": "это не FFprobe", + "errorFFprobeVersion": "Не смогли определить версию FFprobe", + "errorNoFilesAddedForConversion": "Нет файлов для конвертации", + "errorQueue": "Ошибка", + "errorSelectedEncoder": "Конвертер не выбран", + "errorSelectedFolderSave": "Папка для сохранения не выбрана!", + "errorSelectedFormat": "Расширение файла не выбрана", + "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, но будет файл и меньше весить)", + "preset_medium": "medium (медленней чем fast, но будет файл и меньше весить)", + "preset_placebo": "placebo (не рекомендуется)", + "preset_slow": "slow (медленней чем medium, но будет файл и меньше весить)", + "preset_slower": "slower (медленней чем slow, но будет файл и меньше весить)", + "preset_superfast": "superfast (медленней чем ultrafast, но будет файл и меньше весить)", + "preset_ultrafast": "ultrafast (быстро, но файл будет много весить)", + "preset_veryfast": "veryfast (медленней чем superfast, но будет файл и меньше весить)", + "preset_veryslow": "veryslow (медленней чем slower, но будет файл и меньше весить)", + "programmLink": "Сайт проекта", + "programmVersion": "**Версия программы:** {{.Version}}", + "queue": "Очередь", + "save": "Сохранить", + "selectEncoder": "Кодировщик:", + "selectFFPathTitle": "Укажите путь к FFmpeg и к FFprobe", + "selectFormat": "Расширение файла:", + "settings": "Настройки", + "testFF": "Проверка FFmpeg на работоспособность...", + "themesNameDark": "Тёмная", + "themesNameDefault": "По умолчанию", + "themesNameLight": "Светлая", + "titleDownloadLink": "Скачать можно от сюда", + "total": "Всего", + "unzipRun": "Распаковывается...", + "waitingQueue": "В очереди" +} \ No newline at end of file diff --git a/kernel/translations/base.en.json b/kernel/translations/base.en.json new file mode 100644 index 0000000..3f5dd3d --- /dev/null +++ b/kernel/translations/base.en.json @@ -0,0 +1,45 @@ +{ + "Advanced": "Advanced", + "Cancel": "Cancel", + "Confirm": "Confirm", + "Copy": "Copy", + "Create Folder": "Create Folder", + "Cut": "Cut", + "Enter filename": "Enter filename", + "Error": "Error", + "Favourites": "Favourites", + "File": "File", + "Folder": "Folder", + "New Folder": "New Folder", + "No": "No", + "OK": "OK", + "Open": "Open", + "Paste": "Paste", + "Quit": "Quit", + "Redo": "Redo", + "Save": "Save", + "Select all": "Select all", + "Show Hidden Files": "Show Hidden Files", + "Undo": "Undo", + "Yes": "Yes", + "file.name": { + "other": "Name" + }, + "file.parent": { + "other": "Parent" + }, + "friday": "Friday", + "friday.short": "Fri", + "monday": "Monday", + "monday.short": "Mon", + "saturday": "Saturday", + "saturday.short": "Sat", + "sunday": "Sunday", + "sunday.short": "Sun", + "thursday": "Thursday", + "thursday.short": "Thu", + "tuesday": "Tuesday", + "tuesday.short": "Tue", + "wednesday": "Wednesday", + "wednesday.short": "Wed" +} \ No newline at end of file diff --git a/kernel/translations/base.kk.json b/kernel/translations/base.kk.json new file mode 100644 index 0000000..469566b --- /dev/null +++ b/kernel/translations/base.kk.json @@ -0,0 +1,45 @@ +{ + "Advanced": "Кеңейтілген", + "Cancel": "Бас тарту", + "Confirm": "Растау", + "Copy": "Көшіру", + "Create Folder": "Қалта жасау", + "Cut": "Кесу", + "Enter filename": "Файл атауын енгізіңіз", + "Error": "Қате", + "Favourites": "Таңдаулылар", + "File": "Файл", + "Folder": "Қалта", + "New Folder": "Жаңа қалта", + "No": "Жоқ", + "OK": "ОК", + "Open": "Ашу", + "Paste": "Кірістіру", + "Quit": "Шығу", + "Redo": "Қайталау", + "Save": "Сақтау", + "Select all": "Барлығын таңдаңыз", + "Show Hidden Files": "Жасырын файлдарды көрсету", + "Undo": "Бас тарту", + "Yes": "Иә", + "file.name": { + "other": "Аты" + }, + "file.parent": { + "other": "Жоғары" + }, + "friday": "Жұма", + "friday.short": "Жұ", + "monday": "Дүйсенбі", + "monday.short": "Дү", + "saturday": "Сенбі", + "saturday.short": "Сен", + "sunday": "Жексенбі", + "sunday.short": "Же", + "thursday": "Сейсенбі", + "thursday.short": "Се", + "tuesday": "Бейсенбі", + "tuesday.short": "Бе", + "wednesday": "Сәрсенбі", + "wednesday.short": "Сә" +} \ No newline at end of file diff --git a/kernel/translations/base.ru.json b/kernel/translations/base.ru.json new file mode 100644 index 0000000..ac22058 --- /dev/null +++ b/kernel/translations/base.ru.json @@ -0,0 +1,45 @@ +{ + "Advanced": "Расширенные", + "Cancel": "Отмена", + "Confirm": "Подтвердить", + "Copy": "Копировать", + "Create Folder": "Создать папку", + "Cut": "Вырезать", + "Enter filename": "Введите имя файла", + "Error": "Ошибка", + "Favourites": "Избранное", + "File": "Файл", + "Folder": "Папка", + "New Folder": "Новая папка", + "No": "Нет", + "OK": "ОК", + "Open": "Открыть", + "Paste": "Вставить", + "Quit": "Выйти", + "Redo": "Повторить", + "Save": "Сохранить", + "Select all": "Выбрать всё", + "Show Hidden Files": "Показать скрытые файлы", + "Undo": "Отменить", + "Yes": "Да", + "file.name": { + "other": "Имя" + }, + "file.parent": { + "other": "Вверх" + }, + "friday": "Пятница", + "friday.short": "Пт", + "monday": "Понедельник", + "monday.short": "Пн", + "saturday": "Суббота", + "saturday.short": "Сб", + "sunday": "Воскресенье", + "sunday.short": "Вс", + "thursday": "Вторник", + "thursday.short": "Вт", + "tuesday": "Четверг", + "tuesday.short": "Чт", + "wednesday": "Среда", + "wednesday.short": "Ср" +} \ No newline at end of file diff --git a/languages/.gitignore b/languages/.gitignore deleted file mode 100644 index bc8acef..0000000 --- a/languages/.gitignore +++ /dev/null @@ -1 +0,0 @@ -translate.*.toml diff --git a/languages/active.en.toml b/languages/active.en.toml deleted file mode 100644 index a9ab0bc..0000000 --- a/languages/active.en.toml +++ /dev/null @@ -1,563 +0,0 @@ -[AlsoUsedProgram] -hash = "sha1-a72be72e7808bb8a0144ed7a93acb29c568b1ed4" -other = "The program also uses:" - -[about] -hash = "sha1-3da0b9ef719fd707f443ac00404447f29445976f" -other = "About" - -[aboutText] -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" - -[buttonForSelectedDirTitle] -hash = "sha1-8cbe5c67bcf89e4624635a79cbea104227faedda" -other = "Save to folder:" - -[cancel] -hash = "sha1-0ec753be8df955a117404fb634b01b45eb386e2a" -other = "Cancel" - -[changeFFPath] -hash = "sha1-1f704de0560f8135eb6924cd232ed919ca2e5af0" -other = "FFmpeg, FFprobe and FFplay" - -[changeLanguage] -hash = "sha1-8b276eaf378d485c769fb3d5dcc06dfc25b0c01b" -other = "Change language" - -[checkboxOverwriteOutputFilesTitle] -hash = "sha1-5860124bb781e7ef680f573fa93977e96328d4e7" -other = "Allow file to be overwritten" - -[choose] -hash = "sha1-f60bb5f761024d973834b5e9d25ceebce2c85f94" -other = "choose" - -[clearAll] -hash = "sha1-f32702d79ac206432400ac6b041695d020f6fa77" -other = "Clear List" - -[completedQueue] -hash = "sha1-398c7d4f7b0d522afb930769c0fbb1a9f4b61fbe" -other = "Completed" - -[converterVideoFilesSubmitTitle] -hash = "sha1-7ac460f3c24c9952082f2db6e4d62f752598709c" -other = "Convert" - -[converterVideoFilesTitle] -hash = "sha1-1ab29597cc9dfefab08e54ea5442e7ffa15f0394" -other = "Video, audio and picture converter" - -[download] -hash = "sha1-fe8f79f29da457de2f6bc9531de6e536e0c426ad" -other = "Download" - -[downloadFFmpegFromSite] -hash = "sha1-0889c95aa3a8659d8d903b4dab7097699c4d8aa4" -other = "Will be downloaded from the site:" - -[downloadRun] -hash = "sha1-55f87f114628fa2d5d8e67d1e1cda22c0e4f9271" -other = "Downloading..." - -[dragAndDropFiles] -hash = "sha1-07bb747cc7590d7a51cdf96dff49a74139097766" -other = "drag and drop files" - -[encoderGroupAudio] -hash = "sha1-24321cb5400df96be8f3e2131918bebdb3a01bba" -other = "Audio" - -[encoderGroupImage] -hash = "sha1-a7e528bc7ac9538aec87d1593c38b80be95d4745" -other = "Images" - -[encoderGroupVideo] -hash = "sha1-8e7b9894c7ef0f57ac0bf910f6a8aac1c8a53683" -other = "Video" - -[encoder_apng] -hash = "sha1-1cbd9abfef96d5614a7e569161b41bd6ad87bbaf" -other = "APNG image" - -[encoder_bmp] -hash = "sha1-e0b9c16b016961a5abdc2217e8ffd1ba7ddebc40" -other = "BMP image" - -[encoder_flv] -hash = "sha1-3602bbf1cc90e48254f81975c7879b5fc0c4d602" -other = "FLV" - -[encoder_gif] -hash = "sha1-d092a779172291b5215aa095390a5b11659128a4" -other = "GIF image" - -[encoder_h264_nvenc] -hash = "sha1-169389f8c4a2518410159c363378ab5c978c32e5" -other = "H.264 with NVIDIA support" - -[encoder_libmp3lame] -hash = "sha1-cd2c8d6f246c8bc18554b7105cb50b78d3cb2b98" -other = "libmp3lame MP3 (MPEG audio layer 3)" - -[encoder_libshine] -hash = "sha1-891d56c85857e5d83ef5a1fe077c1f1540788f49" -other = "libshine MP3 (MPEG audio layer 3)" - -[encoder_libtwolame] -hash = "sha1-b2f53be810b74edc3c454ac75de7ddecfee322ca" -other = "libtwolame MP2 (MPEG audio layer 2)" - -[encoder_libvpx] -hash = "sha1-b85c923aecfb48de0e87e71b6a21bfc2c547c70e" -other = "libvpx VP8 (codec vp8)" - -[encoder_libvpx-vp9] -hash = "sha1-3106417bd89bee87daa691e87614caf78cb934fe" -other = "libvpx VP9 (codec vp9)" - -[encoder_libwebp] -hash = "sha1-1d590d47d46f7880246061fce0e0de6d743db39e" -other = "libwebp WebP image" - -[encoder_libwebp_anim] -hash = "sha1-f141a9c8f23d79c13d44c30d8f34e05b363771ad" -other = "libwebp_anim WebP image" - -[encoder_libx264] -hash = "sha1-6d764ac459c0bf3c819d76618418cdfbb7a749eb" -other = "H.264 libx264" - -[encoder_libx265] -hash = "sha1-55544c166b1e15fd71a58096518e528109599eea" -other = "H.265 libx265" - -[encoder_libxvid] -hash = "sha1-d4bed46d6cdd2bfa8fd1689801164a83ab10c3f5" -other = "libxvidcore MPEG-4 part 2" - -[encoder_mjpeg] -hash = "sha1-94ba63a322b493a04da65e566781fe1cf8bb0d50" -other = "MJPEG (Motion JPEG)" - -[encoder_mp2] -hash = "sha1-a9154b7203349e5d6fbfd67d1ea97715f54b2065" -other = "MP2 (MPEG audio layer 2)" - -[encoder_mp2fixed] -hash = "sha1-dd2ee670d8bc8a60a96a717ebd26f16b5748cf3f" -other = "MP2 fixed point (MPEG audio layer 2)" - -[encoder_mpeg1video] -hash = "sha1-30043660719a3cb19dab5c33450665a8a9cc1c01" -other = "MPEG-1" - -[encoder_mpeg2video] -hash = "sha1-ccb2dcd8510cfdc9d52e5258af1863e5f2c51e77" -other = "MPEG-2" - -[encoder_mpeg4] -hash = "sha1-67fe42f18421b2f6c90fcdc579f9199bfca4b182" -other = "MPEG-4 part 2" - -[encoder_msmpeg4] -hash = "sha1-313ee597e4f0d9bd63a2bc6ac1618f028aef76f4" -other = "MPEG-4 part 2 Microsoft variant version 3" - -[encoder_msmpeg4v2] -hash = "sha1-adc442ce88f2717693b2da3010a1937d77ee522f" -other = "MPEG-4 part 2 Microsoft variant version 2" - -[encoder_msvideo1] -hash = "sha1-00f43ac0dc162bca10e0d98d6b70c0c6a902f66f" -other = "Microsoft Video-1" - -[encoder_png] -hash = "sha1-6715d4b82f5d9dfe3e53e30b402ffa1a6fbf30a5" -other = "PNG image" - -[encoder_qtrle] -hash = "sha1-31bf155cffaf6842ebc54084e4337ca08fdd9848" -other = "QuickTime Animation (RLE) video" - -[encoder_sgi] -hash = "sha1-f4510e237f7fc3c02caa728f9e500f4b069f9c11" -other = "SGI image" - -[encoder_tiff] -hash = "sha1-ed09d78c38e0b17ed695f35740c756dd7340eeac" -other = "TIFF image" - -[encoder_wmav1] -hash = "sha1-cd4a4c5eeac694b6699d55d0f9b477b3b50f18c7" -other = "Windows Media Audio 1" - -[encoder_wmav2] -hash = "sha1-eb2e5306cb33a702577ecfbdca0461862c66c053" -other = "Windows Media Audio 2" - -[encoder_wmv1] -hash = "sha1-f9b748554c590c36a56bcba2cd317196b7bdeddb" -other = "Windows Media Video 7" - -[encoder_wmv2] -hash = "sha1-5b21c87f5c6104797ead60b488b2948428f6b1b7" -other = "Windows Media Video 8" - -[encoder_xbm] -hash = "sha1-2dfc35881da62e9a1379d8238cf7839b24f79566" -other = "XBM (X BitMap) image" - -[error] -hash = "sha1-a7df8f8b5d754f226ac4cb320577fe692b33e483" -other = "An error has occurred!" - -[errorConverter] -hash = "sha1-55ebddceddb8b044e33cc3893ec2eba7bbd9fcf9" -other = "Couldn't convert video" - -[errorDragAndDropFile] -hash = "sha1-863cf1ad9c820d5b0c2006ceeaa29e25f81c1714" -other = "Not all files were added" - -[errorFFmpeg] -hash = "sha1-ccf0b95c0d1b392dc215258d917eb4e5d0b88ed0" -other = "this is not FFmpeg" - -[errorFFmpegVersion] -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" - -[errorFFprobeVersion] -hash = "sha1-da7b37d7df3fafbd153665b13888413d52b24c17" -other = "Failed to determine FFprobe version" - -[errorNoFilesAddedForConversion] -hash = "sha1-5cf1f65bef15cb0382e56be98f44c6abde56a314" -other = "There are no files to convert" - -[errorQueue] -hash = "sha1-72aecd9ad85642d84d62dbbf3cf70953c5f696c7" -other = "Error" - -[errorSelectedEncoder] -hash = "sha1-33ed1aaf4cb3c2ee9d8f8c325b9b75d16ddf9979" -other = "Converter not selected" - -[errorSelectedFolderSave] -hash = "sha1-16f3ef93ee36813fdd79d8fb9bb7fc02acbb94a8" -other = "No save folder selected!" - -[errorSelectedFormat] -hash = "sha1-cda92c56a1ef1aabc92bbfc405ede8ab13087e66" -other = "File extension not selected" - -[exit] -hash = "sha1-c42457057d1ab7950cea00719cbe0b078891775f" -other = "Exit" - -[ffmpegLGPL] -hash = "sha1-d395b16cc8f8eab98a8a970307c5b010ba22dde6" -other = "This software uses libraries from the **FFmpeg** project under the **[LGPLv2.1](http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html)**." - -[ffmpegTrademark] -hash = "sha1-45f772b2eca5098cd6d31f2d1dc6edec1987a617" -other = "**FFmpeg** is a trademark of **[Fabrice Bellard](http://bellard.org/)**, originator of the **[FFmpeg](https://ffmpeg.org/about.html)** project." - -[fileForConversionTitle] -hash = "sha1-96ac799e1086b31fd8f5f8d4c801829d6c853f08" -other = "File:" - -[fileQueueTitle] -hash = "sha1-aec93b16baeaf55fed871075c9494a460e4a91b8" -other = "Queue" - -[formPreset] -hash = "sha1-7759891ba1ef9f7adc70defc7ac18fbf149c1a68" -other = "Preset" - -[gratitude] -hash = "sha1-51968fc38e53a9a11c861126c62404674fd6096f" -other = "Gratitude" - -[gratitudeText] -hash = "sha1-cb343e4d39ca31e6da6f72b9394cc915cb7d1258" -other = "I sincerely thank you for your invaluable\n\r and timely assistance:" - -[help] -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" - -[languageSelectionFormHead] -hash = "sha1-0ff5fa82cf684112660128cba1711297acf11003" -other = "Switch language" - -[languageSelectionHead] -hash = "sha1-daf1108fc10d3b1a908288d611f749b3cc651e4b" -other = "Choose language" - -[licenseLink] -hash = "sha1-ea18ab849f0eea030d770da82c2a6b3484a7bd13" -other = "License information" - -[licenseLinkOther] -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" - -[parameterCheckbox] -hash = "sha1-9e35221d454870996fd51d576249cf47d1784a3c" -other = "Enable option" - -[pathToFfmpeg] -hash = "sha1-fafc50f1db0f720fe83a96cd70a9e1ad824e96b6" -other = "Path to FFmpeg:" - -[pathToFfplay] -hash = "sha1-5389830dd75a63aa8a5e41e8f07c5fadd8385398" -other = "Path to FFplay:" - -[pathToFfprobe] -hash = "sha1-b872edc9633a2e81ef678dc46fe46a7e91732024" -other = "Path to FFprobe:" - -[preset_fast] -hash = "sha1-935e1ac9d3c8ba4478326c909ba66662acb0540e" -other = "fast (slower than \"faster\", but the file will weigh less)" - -[preset_faster] -hash = "sha1-98620b73c896440c39ea6ec4b9b19d41301c9a7e" -other = "faster (slower than \"veryfast\", but the file will weigh less)" - -[preset_medium] -hash = "sha1-f7d1c30135c22c2f07c247075c0df103bb3c3ea5" -other = "medium (slower than \"fast\", but the file will weigh less)" - -[preset_placebo] -hash = "sha1-7bcff099104bb192881139e6404981bd426b3f91" -other = "placebo (not recommended)" - -[preset_slow] -hash = "sha1-681bf587275a45b48af49bb2ad8f0947919530e7" -other = "slow (slower than \"medium\", but the file will weigh less)" - -[preset_slower] -hash = "sha1-d1c692ee2b7643ae2c71a48bea880327a3c6b1e3" -other = "slower (slower than \"slow\", but the file will weigh less)" - -[preset_superfast] -hash = "sha1-41c39959e8f1547cc9259a5b459c4ccbf368cc23" -other = "superfast (slower than \"ultrafast\", but the file will weigh less)" - -[preset_ultrafast] -hash = "sha1-dfed981573ac2046832f9a9450bc9388958753fa" -other = "ultrafast (fast, but the file will weigh a lot)" - -[preset_veryfast] -hash = "sha1-370b82509887d02d7a2ef9b110df4616b16123ce" -other = "veryfast (slower than \"superfast\", but the file will weigh less)" - -[preset_veryslow] -hash = "sha1-d428bfa6deea9dd5c7c1f80ceba24e123ae96d0d" -other = "veryslow (slower than \"slower\", but the file will weigh less)" - -[programmLink] -hash = "sha1-18f9a3fad6aacefe1b05eed23122800b391ff5ca" -other = "Project website" - -[programmVersion] -hash = "sha1-fa2e4994a301bb24bc2a8fa166e5486ea95a7475" -other = "**Program version:** {{.Version}}" - -[queue] -hash = "sha1-aec93b16baeaf55fed871075c9494a460e4a91b8" -other = "Queue" - -[save] -hash = "sha1-4864057d626a868fa60f999bed3191d61d045ddc" -other = "Save" - -[selectEncoder] -hash = "sha1-88f3670b09758a3336057520a215058d61006abd" -other = "Encoder:" - -[selectFFPathTitle] -hash = "sha1-95581446a28d968ff1a027c623159a7eb08654cf" -other = "Specify the path to FFmpeg and FFprobe" - -[selectFormat] -hash = "sha1-f3809b0b48886570cd4cf1d7099de6da5b6d4524" -other = "File extension:" - -[settings] -hash = "sha1-7f17c7c62a7fd8d1a508481f4778688927734c2f" -other = "Settings" - -[testFF] -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" - -[total] -hash = "sha1-3b5143902e0c5c84459aedf918e17604d9735b94" -other = "Total" - -[unzipRun] -hash = "sha1-c554dad13026668a1f6adf3171837c5d51bbac36" -other = "Unpacked..." - -[waitingQueue] -hash = "sha1-307429dd84150877080c4bbff2b340d1e7dadff2" -other = "Waiting" diff --git a/languages/active.kk.toml b/languages/active.kk.toml deleted file mode 100644 index 100be4c..0000000 --- a/languages/active.kk.toml +++ /dev/null @@ -1,563 +0,0 @@ -[AlsoUsedProgram] -hash = "sha1-a72be72e7808bb8a0144ed7a93acb29c568b1ed4" -other = "Бағдарлама сонымен қатар пайдаланады:" - -[about] -hash = "sha1-3da0b9ef719fd707f443ac00404447f29445976f" -other = "Бағдарлама туралы" - -[aboutText] -hash = "sha1-8bd565814118ba8b90c40eb5b62acf8d2676e7d6" -other = "FFmpeg консоль утилитасы үшін қарапайым интерфейс. \nБірақ мен FFmpeg утилитасының авторы емеспін." - -[addedFilesTitle] -hash = "sha1-8ba0f6e477b0d78df2cc06f1d8b41b888623b851" -other = "Қосылған файлдар" - -[autoClearAfterAddingToQueue] -hash = "sha1-b3781695a4c35380d2cd075bb52f27a2a6d8f19c" -other = "Кезекке қосқаннан кейін тазалаңыз" - -[buttonDownloadFFmpeg] -hash = "sha1-c223c2e15171156192bc3146aee91e6094bb475b" -other = "FFmpeg автоматты түрде жүктеп алыңыз" - -[buttonForSelectedDirTitle] -hash = "sha1-8cbe5c67bcf89e4624635a79cbea104227faedda" -other = "Қалтаға сақтаңыз:" - -[cancel] -hash = "sha1-0ec753be8df955a117404fb634b01b45eb386e2a" -other = "Болдырмау" - -[changeFFPath] -hash = "sha1-1f704de0560f8135eb6924cd232ed919ca2e5af0" -other = "FFmpeg, FFprobe және FFplay" - -[changeLanguage] -hash = "sha1-8b276eaf378d485c769fb3d5dcc06dfc25b0c01b" -other = "Тілді өзгерту" - -[checkboxOverwriteOutputFilesTitle] -hash = "sha1-5860124bb781e7ef680f573fa93977e96328d4e7" -other = "Файлды қайта жазуға рұқсат беріңіз" - -[choose] -hash = "sha1-f60bb5f761024d973834b5e9d25ceebce2c85f94" -other = "таңдау" - -[clearAll] -hash = "sha1-f32702d79ac206432400ac6b041695d020f6fa77" -other = "Тізімді өшіру" - -[completedQueue] -hash = "sha1-398c7d4f7b0d522afb930769c0fbb1a9f4b61fbe" -other = "Дайын" - -[converterVideoFilesSubmitTitle] -hash = "sha1-7ac460f3c24c9952082f2db6e4d62f752598709c" -other = "Файлды түрлендіру" - -[converterVideoFilesTitle] -hash = "sha1-1ab29597cc9dfefab08e54ea5442e7ffa15f0394" -other = "Бейне, аудио және суретті түрлендіргіш" - -[download] -hash = "sha1-fe8f79f29da457de2f6bc9531de6e536e0c426ad" -other = "Жүктеп алу" - -[downloadFFmpegFromSite] -hash = "sha1-0889c95aa3a8659d8d903b4dab7097699c4d8aa4" -other = "Сайттан жүктеледі:" - -[downloadRun] -hash = "sha1-55f87f114628fa2d5d8e67d1e1cda22c0e4f9271" -other = "Жүктеп алынуда..." - -[dragAndDropFiles] -hash = "sha1-07bb747cc7590d7a51cdf96dff49a74139097766" -other = "файлдарды сүйреп апарыңыз" - -[encoderGroupAudio] -hash = "sha1-24321cb5400df96be8f3e2131918bebdb3a01bba" -other = "Аудио" - -[encoderGroupImage] -hash = "sha1-a7e528bc7ac9538aec87d1593c38b80be95d4745" -other = "Суреттер" - -[encoderGroupVideo] -hash = "sha1-8e7b9894c7ef0f57ac0bf910f6a8aac1c8a53683" -other = "Бейне" - -[encoder_apng] -hash = "sha1-1cbd9abfef96d5614a7e569161b41bd6ad87bbaf" -other = "APNG image" - -[encoder_bmp] -hash = "sha1-e0b9c16b016961a5abdc2217e8ffd1ba7ddebc40" -other = "BMP image" - -[encoder_flv] -hash = "sha1-3602bbf1cc90e48254f81975c7879b5fc0c4d602" -other = "FLV" - -[encoder_gif] -hash = "sha1-d092a779172291b5215aa095390a5b11659128a4" -other = "GIF image" - -[encoder_h264_nvenc] -hash = "sha1-169389f8c4a2518410159c363378ab5c978c32e5" -other = "NVIDIA қолдауымен H.264" - -[encoder_libmp3lame] -hash = "sha1-cd2c8d6f246c8bc18554b7105cb50b78d3cb2b98" -other = "libmp3lame MP3 (MPEG audio layer 3)" - -[encoder_libshine] -hash = "sha1-891d56c85857e5d83ef5a1fe077c1f1540788f49" -other = "libshine MP3 (MPEG audio layer 3)" - -[encoder_libtwolame] -hash = "sha1-b2f53be810b74edc3c454ac75de7ddecfee322ca" -other = "libtwolame MP2 (MPEG audio layer 2)" - -[encoder_libvpx] -hash = "sha1-b85c923aecfb48de0e87e71b6a21bfc2c547c70e" -other = "libvpx VP8 (codec vp8)" - -[encoder_libvpx-vp9] -hash = "sha1-3106417bd89bee87daa691e87614caf78cb934fe" -other = "libvpx VP9 (codec vp9)" - -[encoder_libwebp] -hash = "sha1-1d590d47d46f7880246061fce0e0de6d743db39e" -other = "libwebp WebP image" - -[encoder_libwebp_anim] -hash = "sha1-f141a9c8f23d79c13d44c30d8f34e05b363771ad" -other = "libwebp_anim WebP image" - -[encoder_libx264] -hash = "sha1-6d764ac459c0bf3c819d76618418cdfbb7a749eb" -other = "H.264 libx264" - -[encoder_libx265] -hash = "sha1-55544c166b1e15fd71a58096518e528109599eea" -other = "H.265 libx265" - -[encoder_libxvid] -hash = "sha1-d4bed46d6cdd2bfa8fd1689801164a83ab10c3f5" -other = "libxvidcore MPEG-4 part 2" - -[encoder_mjpeg] -hash = "sha1-94ba63a322b493a04da65e566781fe1cf8bb0d50" -other = "MJPEG (Motion JPEG)" - -[encoder_mp2] -hash = "sha1-a9154b7203349e5d6fbfd67d1ea97715f54b2065" -other = "MP2 (MPEG audio layer 2)" - -[encoder_mp2fixed] -hash = "sha1-dd2ee670d8bc8a60a96a717ebd26f16b5748cf3f" -other = "MP2 fixed point (MPEG audio layer 2)" - -[encoder_mpeg1video] -hash = "sha1-30043660719a3cb19dab5c33450665a8a9cc1c01" -other = "MPEG-1" - -[encoder_mpeg2video] -hash = "sha1-ccb2dcd8510cfdc9d52e5258af1863e5f2c51e77" -other = "MPEG-2" - -[encoder_mpeg4] -hash = "sha1-67fe42f18421b2f6c90fcdc579f9199bfca4b182" -other = "MPEG-4 part 2" - -[encoder_msmpeg4] -hash = "sha1-313ee597e4f0d9bd63a2bc6ac1618f028aef76f4" -other = "MPEG-4 part 2 Microsoft variant version 3" - -[encoder_msmpeg4v2] -hash = "sha1-adc442ce88f2717693b2da3010a1937d77ee522f" -other = "MPEG-4 part 2 Microsoft variant version 2" - -[encoder_msvideo1] -hash = "sha1-00f43ac0dc162bca10e0d98d6b70c0c6a902f66f" -other = "Microsoft Video-1" - -[encoder_png] -hash = "sha1-6715d4b82f5d9dfe3e53e30b402ffa1a6fbf30a5" -other = "PNG image" - -[encoder_qtrle] -hash = "sha1-31bf155cffaf6842ebc54084e4337ca08fdd9848" -other = "QuickTime Animation (RLE) video" - -[encoder_sgi] -hash = "sha1-f4510e237f7fc3c02caa728f9e500f4b069f9c11" -other = "SGI image" - -[encoder_tiff] -hash = "sha1-ed09d78c38e0b17ed695f35740c756dd7340eeac" -other = "TIFF image" - -[encoder_wmav1] -hash = "sha1-cd4a4c5eeac694b6699d55d0f9b477b3b50f18c7" -other = "Windows Media Audio 1" - -[encoder_wmav2] -hash = "sha1-eb2e5306cb33a702577ecfbdca0461862c66c053" -other = "Windows Media Audio 2" - -[encoder_wmv1] -hash = "sha1-f9b748554c590c36a56bcba2cd317196b7bdeddb" -other = "Windows Media Video 7" - -[encoder_wmv2] -hash = "sha1-5b21c87f5c6104797ead60b488b2948428f6b1b7" -other = "Windows Media Video 8" - -[encoder_xbm] -hash = "sha1-2dfc35881da62e9a1379d8238cf7839b24f79566" -other = "XBM (X BitMap) image" - -[error] -hash = "sha1-a7df8f8b5d754f226ac4cb320577fe692b33e483" -other = "Қате орын алды!" - -[errorConverter] -hash = "sha1-55ebddceddb8b044e33cc3893ec2eba7bbd9fcf9" -other = "Бейнені түрлендіру мүмкін болмады" - -[errorDragAndDropFile] -hash = "sha1-863cf1ad9c820d5b0c2006ceeaa29e25f81c1714" -other = "Барлық файлдар қосылмаған" - -[errorFFmpeg] -hash = "sha1-ccf0b95c0d1b392dc215258d917eb4e5d0b88ed0" -other = "бұл FFmpeg емес" - -[errorFFmpegVersion] -hash = "sha1-9a4148d42186b6b32cf83bef726e23022c53283f" -other = "FFmpeg нұсқасын анықтау мүмкін болмады" - -[errorFFplay] -hash = "sha1-988122112ac6002094e25518cfb5f0d606217298" -other = "бұл FFplay емес" - -[errorFFplayVersion] -hash = "sha1-cd60928d20d93210e103dd464306ab138bf1b184" -other = "FFplay нұсқасын анықтау мүмкін болмады" - -[errorFFprobe] -hash = "sha1-86d1b0b4c4ccd6a4f71e758fc67ce11aff4ba9b8" -other = "бұл FFprobe емес" - -[errorFFprobeVersion] -hash = "sha1-da7b37d7df3fafbd153665b13888413d52b24c17" -other = "FFprobe нұсқасын анықтау мүмкін болмады" - -[errorNoFilesAddedForConversion] -hash = "sha1-5cf1f65bef15cb0382e56be98f44c6abde56a314" -other = "Түрлендіруге арналған файлдар жоқ" - -[errorQueue] -hash = "sha1-72aecd9ad85642d84d62dbbf3cf70953c5f696c7" -other = "Қате" - -[errorSelectedEncoder] -hash = "sha1-33ed1aaf4cb3c2ee9d8f8c325b9b75d16ddf9979" -other = "Түрлендіргіш таңдалмаған" - -[errorSelectedFolderSave] -hash = "sha1-16f3ef93ee36813fdd79d8fb9bb7fc02acbb94a8" -other = "Сақтау қалтасы таңдалмаған!" - -[errorSelectedFormat] -hash = "sha1-cda92c56a1ef1aabc92bbfc405ede8ab13087e66" -other = "Файл кеңейтімі таңдалмаған" - -[exit] -hash = "sha1-c42457057d1ab7950cea00719cbe0b078891775f" -other = "Шығу" - -[ffmpegLGPL] -hash = "sha1-d395b16cc8f8eab98a8a970307c5b010ba22dde6" -other = "Бұл бағдарламалық құрал **[LGPLv2.1](http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html)** астында **FFmpeg** жобасының кітапханаларын пайдаланады." - -[ffmpegTrademark] -hash = "sha1-45f772b2eca5098cd6d31f2d1dc6edec1987a617" -other = "FFmpeg — **[FFmpeg](https://ffmpeg.org/about.html)** жобасын жасаушы **[Fabrice Bellard](http://bellard.org/)** сауда белгісі." - -[fileForConversionTitle] -hash = "sha1-96ac799e1086b31fd8f5f8d4c801829d6c853f08" -other = "Файл:" - -[fileQueueTitle] -hash = "sha1-aec93b16baeaf55fed871075c9494a460e4a91b8" -other = "Кезек" - -[formPreset] -hash = "sha1-7759891ba1ef9f7adc70defc7ac18fbf149c1a68" -other = "Алдын ала орнатылған" - -[gratitude] -hash = "sha1-51968fc38e53a9a11c861126c62404674fd6096f" -other = "Алғыс" - -[gratitudeText] -hash = "sha1-cb343e4d39ca31e6da6f72b9394cc915cb7d1258" -other = "Сізге баға жетпес және уақтылы көмектескеніңіз\n\r үшін шын жүректен алғыс айтамын:" - -[help] -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 = "Орындалуда" - -[languageSelectionFormHead] -hash = "sha1-0ff5fa82cf684112660128cba1711297acf11003" -other = "Тілді ауыстыру" - -[languageSelectionHead] -hash = "sha1-daf1108fc10d3b1a908288d611f749b3cc651e4b" -other = "Тілді таңдаңыз" - -[licenseLink] -hash = "sha1-ea18ab849f0eea030d770da82c2a6b3484a7bd13" -other = "Лицензия туралы ақпарат" - -[licenseLinkOther] -hash = "sha1-359fff328717c05104e51a2d29f05bf1875d26b7" -other = "Бағдарламада пайдаланылатын басқа өнімдердің лицензиялары" - -[menuSettingsLanguage] -hash = "sha1-ed3f0e507a5b4ed0649d7c768fe0d47413d839ba" -other = "Тіл" - -[menuSettingsTheme] -hash = "sha1-553c45f1b84a92b08dc1f088c13f924cde95765e" -other = "Тақырып" - -[or] -hash = "sha1-30bb0333ca1583110e4ced513b5d2455b86f529b" -other = "немесе" - -[parameterCheckbox] -hash = "sha1-9e35221d454870996fd51d576249cf47d1784a3c" -other = "Опцияны қосу" - -[pathToFfmpeg] -hash = "sha1-fafc50f1db0f720fe83a96cd70a9e1ad824e96b6" -other = "FFmpeg жол:" - -[pathToFfplay] -hash = "sha1-5389830dd75a63aa8a5e41e8f07c5fadd8385398" -other = "FFplay жол:" - -[pathToFfprobe] -hash = "sha1-b872edc9633a2e81ef678dc46fe46a7e91732024" -other = "FFprobe жол:" - -[preset_fast] -hash = "sha1-935e1ac9d3c8ba4478326c909ba66662acb0540e" -other = "fast («faster» қарағанда баяуырақ, бірақ файлдың салмағы аз болады)" - -[preset_faster] -hash = "sha1-98620b73c896440c39ea6ec4b9b19d41301c9a7e" -other = "faster («veryfast» қарағанда баяуырақ, бірақ файлдың салмағы аз болады)" - -[preset_medium] -hash = "sha1-f7d1c30135c22c2f07c247075c0df103bb3c3ea5" -other = "medium («fast» қарағанда баяуырақ, бірақ файлдың салмағы аз болады)" - -[preset_placebo] -hash = "sha1-7bcff099104bb192881139e6404981bd426b3f91" -other = "placebo (ұсынылмайды)" - -[preset_slow] -hash = "sha1-681bf587275a45b48af49bb2ad8f0947919530e7" -other = "slow («medium» қарағанда баяуырақ, бірақ файлдың салмағы аз болады)" - -[preset_slower] -hash = "sha1-d1c692ee2b7643ae2c71a48bea880327a3c6b1e3" -other = "slower («slow» қарағанда баяуырақ, бірақ файлдың салмағы аз болады)" - -[preset_superfast] -hash = "sha1-41c39959e8f1547cc9259a5b459c4ccbf368cc23" -other = "superfast («ultrafast» қарағанда баяуырақ, бірақ файлдың салмағы аз болады)" - -[preset_ultrafast] -hash = "sha1-dfed981573ac2046832f9a9450bc9388958753fa" -other = "ultrafast (жылдам, бірақ файлдың салмағы көп болады)" - -[preset_veryfast] -hash = "sha1-370b82509887d02d7a2ef9b110df4616b16123ce" -other = "veryfast («superfast» қарағанда баяуырақ, бірақ файлдың салмағы аз болады)" - -[preset_veryslow] -hash = "sha1-d428bfa6deea9dd5c7c1f80ceba24e123ae96d0d" -other = "veryslow («slower» қарағанда баяуырақ, бірақ файлдың салмағы аз болады)" - -[programmLink] -hash = "sha1-18f9a3fad6aacefe1b05eed23122800b391ff5ca" -other = "Жобаның веб-сайты" - -[programmVersion] -hash = "sha1-fa2e4994a301bb24bc2a8fa166e5486ea95a7475" -other = "**Бағдарлама нұсқасы:** {{.Version}}" - -[queue] -hash = "sha1-aec93b16baeaf55fed871075c9494a460e4a91b8" -other = "Кезек" - -[save] -hash = "sha1-4864057d626a868fa60f999bed3191d61d045ddc" -other = "Сақтау" - -[selectEncoder] -hash = "sha1-88f3670b09758a3336057520a215058d61006abd" -other = "Кодировщик:" - -[selectFFPathTitle] -hash = "sha1-95581446a28d968ff1a027c623159a7eb08654cf" -other = "FFmpeg және FFprobe жолын көрсетіңіз" - -[selectFormat] -hash = "sha1-f3809b0b48886570cd4cf1d7099de6da5b6d4524" -other = "Файл кеңейтімі:" - -[settings] -hash = "sha1-7f17c7c62a7fd8d1a508481f4778688927734c2f" -other = "Параметрлер" - -[testFF] -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 = "Сіз оны осы жерден жүктей аласыз" - -[total] -hash = "sha1-3b5143902e0c5c84459aedf918e17604d9735b94" -other = "Барлығы" - -[unzipRun] -hash = "sha1-c554dad13026668a1f6adf3171837c5d51bbac36" -other = "Орамнан шығарылуда..." - -[waitingQueue] -hash = "sha1-307429dd84150877080c4bbff2b340d1e7dadff2" -other = "Күту" diff --git a/languages/active.ru.toml b/languages/active.ru.toml deleted file mode 100644 index 0609a33..0000000 --- a/languages/active.ru.toml +++ /dev/null @@ -1,141 +0,0 @@ -AlsoUsedProgram = "Также в программе используется:" -about = "О программе" -aboutText = "Простенький интерфейс для консольной утилиты FFmpeg. \nНо я не являюсь автором самой утилиты FFmpeg." -addedFilesTitle = "Добавленные файлы" -autoClearAfterAddingToQueue = "Очищать после добавления в очередь" -buttonDownloadFFmpeg = "Скачать автоматически FFmpeg" -buttonForSelectedDirTitle = "Сохранить в папку:" -cancel = "Отмена" -changeFFPath = "FFmpeg, FFprobe и FFplay" -changeLanguage = "Поменять язык" -checkboxOverwriteOutputFilesTitle = "Разрешить перезаписать файл" -choose = "выбрать" -clearAll = "Очистить список" -completedQueue = "Готово" -converterVideoFilesSubmitTitle = "Конвертировать" -converterVideoFilesTitle = "Конвертер видео, аудио и картинок" -download = "Скачать" -downloadFFmpegFromSite = "Будет скачано с сайта:" -downloadRun = "Скачивается..." -dragAndDropFiles = "перетащить файлы" -encoderGroupAudio = "Аудио" -encoderGroupImage = "Картинки" -encoderGroupVideo = "Видео" -encoder_apng = "APNG image" -encoder_bmp = "BMP image" -encoder_flv = "FLV" -encoder_gif = "GIF image" -encoder_h264_nvenc = "H.264 с поддержкой NVIDIA" -encoder_libmp3lame = "libmp3lame MP3 (MPEG audio layer 3)" -encoder_libshine = "libshine MP3 (MPEG audio layer 3)" -encoder_libtwolame = "libtwolame MP2 (MPEG audio layer 2)" -encoder_libvpx = "libvpx VP8 (codec vp8)" -encoder_libvpx-vp9 = "libvpx VP9 (codec vp9)" -encoder_libwebp = "libwebp WebP image" -encoder_libwebp_anim = "libwebp_anim WebP image" -encoder_libx264 = "H.264 libx264" -encoder_libx265 = "H.265 libx265" -encoder_libxvid = "libxvidcore MPEG-4 part 2" -encoder_mjpeg = "MJPEG (Motion JPEG)" -encoder_mp2 = "MP2 (MPEG audio layer 2)" -encoder_mp2fixed = "MP2 fixed point (MPEG audio layer 2)" -encoder_mpeg1video = "MPEG-1" -encoder_mpeg2video = "MPEG-2" -encoder_mpeg4 = "MPEG-4 part 2" -encoder_msmpeg4 = "MPEG-4 part 2 Microsoft variant version 3" -encoder_msmpeg4v2 = "MPEG-4 part 2 Microsoft variant version 2" -encoder_msvideo1 = "Microsoft Video-1" -encoder_png = "PNG image" -encoder_qtrle = "QuickTime Animation (RLE) video" -encoder_sgi = "SGI image" -encoder_tiff = "TIFF image" -encoder_wmav1 = "Windows Media Audio 1" -encoder_wmav2 = "Windows Media Audio 2" -encoder_wmv1 = "Windows Media Video 7" -encoder_wmv2 = "Windows Media Video 8" -encoder_xbm = "XBM (X BitMap) image" -error = "Произошла ошибка!" -errorConverter = "не смогли отконвертировать видео" -errorDragAndDropFile = "Не все файлы добавились" -errorFFmpeg = "это не FFmpeg" -errorFFmpegVersion = "Не смогли определить версию FFmpeg" -errorFFplay = "это не FFplay" -errorFFplayVersion = "Не смогли определить версию FFplay" -errorFFprobe = "это не FFprobe" -errorFFprobeVersion = "Не смогли определить версию FFprobe" -errorNoFilesAddedForConversion = "Нет файлов для конвертации" -errorQueue = "Ошибка" -errorSelectedEncoder = "Конвертер не выбран" -errorSelectedFolderSave = "Папка для сохранения не выбрана!" -errorSelectedFormat = "Расширение файла не выбрана" -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, но будет файл и меньше весить)" -preset_medium = "medium (медленней чем fast, но будет файл и меньше весить)" -preset_placebo = "placebo (не рекомендуется)" -preset_slow = "slow (медленней чем medium, но будет файл и меньше весить)" -preset_slower = "slower (медленней чем slow, но будет файл и меньше весить)" -preset_superfast = "superfast (медленней чем ultrafast, но будет файл и меньше весить)" -preset_ultrafast = "ultrafast (быстро, но файл будет много весить)" -preset_veryfast = "veryfast (медленней чем superfast, но будет файл и меньше весить)" -preset_veryslow = "veryslow (медленней чем slower, но будет файл и меньше весить)" -programmLink = "Сайт проекта" -programmVersion = "**Версия программы:** {{.Version}}" -queue = "Очередь" -save = "Сохранить" -selectEncoder = "Кодировщик:" -selectFFPathTitle = "Укажите путь к FFmpeg и к FFprobe" -selectFormat = "Расширение файла:" -settings = "Настройки" -testFF = "Проверка FFmpeg на работоспособность..." -themesNameDark = "Тёмная" -themesNameDefault = "По умолчанию" -themesNameLight = "Светлая" -titleDownloadLink = "Скачать можно от сюда" -total = "Всего" -unzipRun = "Распаковывается..." -waitingQueue = "В очереди" diff --git a/localizer/repository.go b/localizer/repository.go deleted file mode 100644 index eed9051..0000000 --- a/localizer/repository.go +++ /dev/null @@ -1,26 +0,0 @@ -package localizer - -import ( - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/setting" -) - -type RepositoryContract interface { - GetCode() string - Save(code string) setting.Setting -} - -type Repository struct { - settingRepository setting.RepositoryContract -} - -func NewRepository(settingRepository setting.RepositoryContract) *Repository { - return &Repository{settingRepository: settingRepository} -} - -func (r Repository) GetCode() string { - return r.settingRepository.GetValue("language") -} - -func (r Repository) Save(code string) setting.Setting { - return r.settingRepository.CreateOrUpdate("language", code) -} diff --git a/localizer/view.go b/localizer/view.go index c0e5af6..9233b6d 100644 --- a/localizer/view.go +++ b/localizer/view.go @@ -4,7 +4,6 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/widget" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" - "github.com/nicksnyder/go-i18n/v2/i18n" ) type ViewContract interface { @@ -35,13 +34,11 @@ func (v View) LanguageSelection(funcSelected func(lang kernel.Lang)) { block.SetText(languages[i].Title) }) listView.OnSelected = func(id widget.ListItemID) { - _ = v.app.GetLocalizerService().SetCurrentLanguage(languages[id]) + _ = v.app.GetLocalizerService().SetCurrentLanguage(languages[id], true) funcSelected(languages[id]) } - messageHead := v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "languageSelectionHead", - }) + messageHead := v.app.GetLocalizerService().GetMessage("languageSelectionHead") v.app.GetWindow().SetContent(widget.NewCard(messageHead, "", listView)) } @@ -58,17 +55,15 @@ func LanguageSelectionForm(localizerService kernel.LocalizerContract, funcSelect func(i widget.ListItemID, o fyne.CanvasObject) { block := o.(*widget.Label) block.SetText(languages[i].Title) - if languages[i].Code == currentLanguage.Lang.Code { + if languages[i].Code == currentLanguage.Code { block.TextStyle = fyne.TextStyle{Bold: true} } }) listView.OnSelected = func(id widget.ListItemID) { - _ = localizerService.SetCurrentLanguage(languages[id]) + _ = localizerService.SetCurrentLanguage(languages[id], true) funcSelected(languages[id]) } - messageHead := localizerService.GetMessage(&i18n.LocalizeConfig{ - MessageID: "languageSelectionFormHead", - }) + messageHead := localizerService.GetMessage("languageSelectionFormHead") return widget.NewCard(messageHead, "", listView) } diff --git a/main.go b/main.go index 38c2315..1c67187 100644 --- a/main.go +++ b/main.go @@ -10,7 +10,6 @@ import ( "git.kor-elf.net/kor-elf/gui-for-ffmpeg/menu" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/setting" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/theme" - "golang.org/x/text/language" ) var application kernel.AppContract @@ -25,12 +24,6 @@ func init() { Icon: iconResource, } - localizerService, err := kernel.NewLocalizer("languages", language.Russian) - if err != nil { - kernel.PanicErrorLang(err, appMetadata) - return - } - ffPathUtilities = &kernel.FFPathUtilities{FFmpeg: "", FFprobe: "", FFplay: ""} convertorService := kernel.NewService(ffPathUtilities) ffplayService := kernel.NewFFplay(ffPathUtilities) @@ -38,7 +31,6 @@ func init() { queue := kernel.NewQueueList() application = kernel.NewApp( appMetadata, - localizerService, queue, ffplayService, convertorService, @@ -71,12 +63,11 @@ func main() { themeRepository := theme.NewRepository(settingRepository) themeService := theme.NewTheme(application, themeRepository) - localizerRepository := localizer.NewRepository(settingRepository) menuView := menu.NewView(application) menuSettingView := menu.NewViewSetting(application, themeService) - mainMenu := handler.NewMenuHandler(application, convertorHandler, menuView, menuSettingView, localizerView, localizerRepository, themeService) + mainMenu := handler.NewMenuHandler(application, convertorHandler, menuView, menuSettingView, localizerView, themeService) - mainHandler := handler.NewMainHandler(application, convertorHandler, mainMenu, localizerRepository) + mainHandler := handler.NewMainHandler(application, convertorHandler, mainMenu) mainHandler.Start() application.GetWindow().SetMainMenu(mainMenu.GetMainMenu()) diff --git a/menu/view.go b/menu/view.go index 324b012..29864d8 100644 --- a/menu/view.go +++ b/menu/view.go @@ -7,7 +7,6 @@ import ( "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" "golang.org/x/image/colornames" "net/url" ) @@ -29,9 +28,7 @@ func NewView(app kernel.AppContract) *View { } func (v View) Gratitude() { - view := v.app.GetAppFyne().NewWindow(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "gratitude", - })) + view := v.app.GetAppFyne().NewWindow(v.app.GetLocalizerService().GetMessage("gratitude")) view.Resize(fyne.Size{Width: 500, Height: 400}) view.SetFixedSize(true) @@ -39,9 +36,7 @@ func (v View) Gratitude() { image.SetMinSize(fyne.Size{Width: 100, Height: 100}) image.FillMode = canvas.ImageFillContain - gratitude := canvas.NewText(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "gratitude", - }), colornames.Darkgreen) + gratitude := canvas.NewText(v.app.GetLocalizerService().GetMessage("gratitude"), colornames.Darkgreen) gratitude.TextStyle = fyne.TextStyle{Bold: true} gratitude.TextSize = 20 @@ -49,13 +44,11 @@ func (v View) Gratitude() { container.NewScroll(container.NewVBox( container.NewBorder(nil, nil, container.NewVBox(image), nil, container.NewVBox( gratitude, - widget.NewLabel(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "gratitudeText", - })), + widget.NewLabel(v.app.GetLocalizerService().GetMessage("gratitudeText")), widget.NewLabel("Екатерина"), widget.NewLabel("Евгений"), - ), - ))), + )), + )), ) view.CenterOnScreen() @@ -63,9 +56,7 @@ func (v View) Gratitude() { } func (v View) About(ffmpegVersion string, ffprobeVersion string, ffplayVersion string) { - view := v.app.GetAppFyne().NewWindow(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "about", - })) + view := v.app.GetAppFyne().NewWindow(v.app.GetLocalizerService().GetMessage("about")) view.Resize(fyne.Size{Width: 793, Height: 550}) view.SetFixedSize(true) @@ -73,54 +64,51 @@ func (v View) About(ffmpegVersion string, ffprobeVersion string, ffplayVersion s 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: "gui-for-ffmpeg.projects.kor-elf.net", - Path: "/", - }) - - licenseLink := widget.NewHyperlink(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "licenseLink", - }), &url.URL{ - Scheme: "https", - Host: "git.kor-elf.net", - Path: "kor-elf/gui-for-ffmpeg/src/branch/main/LICENSE", - }) - - licenseLinkOther := widget.NewHyperlink(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "licenseLinkOther", - }), &url.URL{ - Scheme: "https", - Host: "git.kor-elf.net", - Path: "kor-elf/gui-for-ffmpeg/src/branch/main/LICENSE-3RD-PARTY.txt", - }) - - programmVersion := widget.NewRichTextFromMarkdown(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "programmVersion", - TemplateData: map[string]string{ - "Version": v.app.GetAppFyne().Metadata().Version, + programmLink := widget.NewHyperlink( + v.app.GetLocalizerService().GetMessage("programmLink"), + &url.URL{ + Scheme: "https", + Host: "gui-for-ffmpeg.projects.kor-elf.net", + Path: "/", }, - })) + ) + + licenseLink := widget.NewHyperlink( + v.app.GetLocalizerService().GetMessage("licenseLink"), + &url.URL{ + Scheme: "https", + Host: "git.kor-elf.net", + Path: "kor-elf/gui-for-ffmpeg/src/branch/main/LICENSE", + }, + ) + + licenseLinkOther := widget.NewHyperlink(v.app.GetLocalizerService().GetMessage( + "licenseLinkOther"), + &url.URL{ + Scheme: "https", + Host: "git.kor-elf.net", + Path: "kor-elf/gui-for-ffmpeg/src/branch/main/LICENSE-3RD-PARTY.txt", + }, + ) + + programmVersion := widget.NewRichTextFromMarkdown( + v.app.GetLocalizerService().GetMessage( + "programmVersion", + map[string]any{"Version": v.app.GetAppFyne().Metadata().Version}, + ), + ) aboutText := widget.NewRichText( &widget.TextSegment{ - Text: v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "aboutText", - }), + Text: v.app.GetLocalizerService().GetMessage("aboutText"), }, ) image := canvas.NewImageFromFile("icon.png") image.SetMinSize(fyne.Size{Width: 100, Height: 100}) image.FillMode = canvas.ImageFillContain - ffmpegTrademark := widget.NewRichTextFromMarkdown(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "ffmpegTrademark", - })) - ffmpegLGPL := widget.NewRichTextFromMarkdown(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "ffmpegLGPL", - })) + ffmpegTrademark := widget.NewRichTextFromMarkdown(v.app.GetLocalizerService().GetMessage("ffmpegTrademark")) + ffmpegLGPL := widget.NewRichTextFromMarkdown(v.app.GetLocalizerService().GetMessage("ffmpegLGPL")) view.SetContent( container.NewScroll(container.NewVBox( @@ -137,9 +125,7 @@ func (v View) About(ffmpegVersion string, ffprobeVersion string, ffplayVersion s v.getAboutFfmpeg(ffmpegVersion), v.getAboutFfprobe(ffprobeVersion), v.getAboutFfplay(ffplayVersion), - widget.NewCard(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "AlsoUsedProgram", - }), "", v.getOther()), + widget.NewCard(v.app.GetLocalizerService().GetMessage("AlsoUsedProgram"), "", v.getOther()), )), ) view.CenterOnScreen() @@ -147,119 +133,71 @@ func (v View) About(ffmpegVersion string, ffprobeVersion string, ffplayVersion s } func (v View) HelpFFplay() { - view := v.app.GetAppFyne().NewWindow(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "helpFFplay", - })) + view := v.app.GetAppFyne().NewWindow(v.app.GetLocalizerService().GetMessage("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", - }), + v.app.GetLocalizerService().GetMessage("helpFFplayKeys"), + v.app.GetLocalizerService().GetMessage("helpFFplayDescription"), }, []string{ "Q, ESC", - v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "helpFFplayQuit", - }), + v.app.GetLocalizerService().GetMessage("helpFFplayQuit"), }, []string{ - "F, " + v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "helpFFplayDoubleClickLeftMouseButton", - }), - v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "helpFFplayToggleFullScreen", - }), + "F, " + v.app.GetLocalizerService().GetMessage("helpFFplayDoubleClickLeftMouseButton"), + v.app.GetLocalizerService().GetMessage("helpFFplayToggleFullScreen"), }, []string{ "P, " + - v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "helpFFplayKeySpace", - }), - v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "helpFFplayPause", - }), + v.app.GetLocalizerService().GetMessage("helpFFplayKeySpace"), + v.app.GetLocalizerService().GetMessage("helpFFplayPause"), }, []string{ "M", - v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "helpFFplayToggleMute", - }), + v.app.GetLocalizerService().GetMessage("helpFFplayToggleMute"), }, []string{ "9, /", - v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "helpFFplayDecreaseVolume", - }), + v.app.GetLocalizerService().GetMessage("helpFFplayDecreaseVolume"), }, []string{ "0, *", - v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "helpFFplayIncreaseVolume", - }), + v.app.GetLocalizerService().GetMessage("helpFFplayIncreaseVolume"), }, []string{ - v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "helpFFplayKeyLeft", - }), - v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "helpFFplaySeekBackward10Seconds", - }), + v.app.GetLocalizerService().GetMessage("helpFFplayKeyLeft"), + v.app.GetLocalizerService().GetMessage("helpFFplaySeekBackward10Seconds"), }, []string{ - v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "helpFFplayKeyRight", - }), - v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "helpFFplaySeekForward10Seconds", - }), + v.app.GetLocalizerService().GetMessage("helpFFplayKeyRight"), + v.app.GetLocalizerService().GetMessage("helpFFplaySeekForward10Seconds"), }, []string{ - v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "helpFFplayKeyDown", - }), - v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "helpFFplaySeekBackward1Minute", - }), + v.app.GetLocalizerService().GetMessage("helpFFplayKeyDown"), + v.app.GetLocalizerService().GetMessage("helpFFplaySeekBackward1Minute"), }, []string{ - v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "helpFFplayKeyUp", - }), - v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "helpFFplaySeekBForward1Minute", - }), + v.app.GetLocalizerService().GetMessage("helpFFplayKeyUp"), + v.app.GetLocalizerService().GetMessage("helpFFplaySeekBForward1Minute"), }, []string{ "Page Down", - v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "helpFFplaySeekBackward10Minutes", - }), + v.app.GetLocalizerService().GetMessage("helpFFplaySeekBackward10Minutes"), }, []string{ "Page Up", - v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "helpFFplaySeekBForward10Minutes", - }), + v.app.GetLocalizerService().GetMessage("helpFFplaySeekBForward10Minutes"), }, []string{ - "S, " + v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "helpFFplayKeyHoldS", - }), - v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "helpFFplayActivateFrameStepMode", - }), + "S, " + v.app.GetLocalizerService().GetMessage("helpFFplayKeyHoldS"), + v.app.GetLocalizerService().GetMessage("helpFFplayActivateFrameStepMode"), }, []string{ "W", - v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "helpFFplayCycleVideoFiltersOrShowModes", - }), + v.app.GetLocalizerService().GetMessage("helpFFplayCycleVideoFiltersOrShowModes"), }, } @@ -300,17 +238,13 @@ func (v View) getAboutFfmpeg(version string) *fyne.Container { programmName.TextStyle = fyne.TextStyle{Bold: true} programmName.TextSize = 20 - programmLink := widget.NewHyperlink(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "programmLink", - }), &url.URL{ + programmLink := widget.NewHyperlink(v.app.GetLocalizerService().GetMessage("programmLink"), &url.URL{ Scheme: "https", Host: "ffmpeg.org", Path: "", }) - licenseLink := widget.NewHyperlink(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "licenseLink", - }), &url.URL{ + licenseLink := widget.NewHyperlink(v.app.GetLocalizerService().GetMessage("licenseLink"), &url.URL{ Scheme: "https", Host: "ffmpeg.org", Path: "legal.html", @@ -330,17 +264,13 @@ func (v View) getAboutFfprobe(version string) *fyne.Container { programmName.TextStyle = fyne.TextStyle{Bold: true} programmName.TextSize = 20 - programmLink := widget.NewHyperlink(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "programmLink", - }), &url.URL{ + programmLink := widget.NewHyperlink(v.app.GetLocalizerService().GetMessage("programmLink"), &url.URL{ Scheme: "https", Host: "ffmpeg.org", Path: "ffprobe.html", }) - licenseLink := widget.NewHyperlink(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "licenseLink", - }), &url.URL{ + licenseLink := widget.NewHyperlink(v.app.GetLocalizerService().GetMessage("licenseLink"), &url.URL{ Scheme: "https", Host: "ffmpeg.org", Path: "legal.html", @@ -360,17 +290,13 @@ func (v View) getAboutFfplay(version string) *fyne.Container { programmName.TextStyle = fyne.TextStyle{Bold: true} programmName.TextSize = 20 - programmLink := widget.NewHyperlink(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "programmLink", - }), &url.URL{ + programmLink := widget.NewHyperlink(v.app.GetLocalizerService().GetMessage("programmLink"), &url.URL{ Scheme: "https", Host: "ffmpeg.org", Path: "ffplay.html", }) - licenseLink := widget.NewHyperlink(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "licenseLink", - }), &url.URL{ + licenseLink := widget.NewHyperlink(v.app.GetLocalizerService().GetMessage("licenseLink"), &url.URL{ Scheme: "https", Host: "ffmpeg.org", Path: "legal.html", diff --git a/menu/view_setting.go b/menu/view_setting.go index 045662e..02e7fbd 100644 --- a/menu/view_setting.go +++ b/menu/view_setting.go @@ -6,7 +6,6 @@ import ( "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" ) @@ -40,7 +39,7 @@ func (v ViewSetting) Main(save func(*SettingForm) error, cancel func()) { errorMessage.TextStyle = fyne.TextStyle{Bold: true} viewSettingForm := &SettingForm{ - Language: v.app.GetLocalizerService().GetCurrentLanguage().Lang, + Language: v.app.GetLocalizerService().GetCurrentLanguage(), ThemeInfo: v.themeService.GetCurrentThemeInfo(), } @@ -55,7 +54,7 @@ func (v ViewSetting) Main(save func(*SettingForm) error, cancel func()) { viewSettingForm.Language = lang } }) - selectLanguage.Selected = v.app.GetLocalizerService().GetCurrentLanguage().Lang.Title + selectLanguage.Selected = v.app.GetLocalizerService().GetCurrentLanguage().Title themeItems := []string{} themeByTitle := map[string]theme.ThemeInfoContract{} @@ -73,24 +72,18 @@ func (v ViewSetting) Main(save func(*SettingForm) error, cancel func()) { form := &widget.Form{ Items: []*widget.FormItem{ { - Text: v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "menuSettingsLanguage", - }), + Text: v.app.GetLocalizerService().GetMessage("menuSettingsLanguage"), Widget: selectLanguage, }, { - Text: v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "menuSettingsTheme", - }), + Text: v.app.GetLocalizerService().GetMessage("menuSettingsTheme"), Widget: selectTheme, }, { Widget: errorMessage, }, }, - SubmitText: v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "save", - }), + SubmitText: v.app.GetLocalizerService().GetMessage("save"), OnSubmit: func() { err := save(viewSettingForm) if err != nil { @@ -100,13 +93,9 @@ func (v ViewSetting) Main(save func(*SettingForm) error, cancel func()) { } if cancel != nil { form.OnCancel = cancel - form.CancelText = v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "cancel", - }) + form.CancelText = v.app.GetLocalizerService().GetMessage("cancel") } - messageHead := v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ - MessageID: "settings", - }) + messageHead := v.app.GetLocalizerService().GetMessage("settings") v.app.GetWindow().SetContent(widget.NewCard(messageHead, "", form)) } diff --git a/theme/theme.go b/theme/theme.go index 2c87899..1b4aa8b 100644 --- a/theme/theme.go +++ b/theme/theme.go @@ -4,7 +4,6 @@ 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" ) @@ -92,25 +91,19 @@ func (inf themeInfo) GetVariant() fyne.ThemeVariant { func getThemes(localizer kernel.LocalizerContract) map[string]ThemeInfoContract { themesNameDefault := &themeInfo{ - name: "default", - title: localizer.GetMessage(&i18n.LocalizeConfig{ - MessageID: "themesNameDefault", - }), + name: "default", + title: localizer.GetMessage("themesNameDefault"), } themesNameLight := &themeInfo{ - name: "light", - title: localizer.GetMessage(&i18n.LocalizeConfig{ - MessageID: "themesNameLight", - }), + name: "light", + title: localizer.GetMessage("themesNameLight"), variant: fyneTheme.VariantLight, } themesNameDark := &themeInfo{ - name: "dark", - title: localizer.GetMessage(&i18n.LocalizeConfig{ - MessageID: "themesNameDark", - }), + name: "dark", + title: localizer.GetMessage("themesNameDark"), variant: fyneTheme.VariantDark, } -- 2.47.2 From 43d794373a341b7246e306c19c1438742859c583 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sun, 1 Jun 2025 19:58:28 +0500 Subject: [PATCH 03/41] Embed application icon as a resource and refactor icon usage Replaced the external `icon.png` file with an embedded resource using Go's embed functionality. Updated all references to use the new resource, eliminating the need for the standalone icon file. --- README.md | 1 - images/screenshot-folder-structure.png | Bin 5891 -> 5659 bytes internal/resources/icon.go | 13 +++++++++++++ internal/resources/icons/logo.png | Bin 0 -> 29642 bytes main.go | 5 +++-- menu/view.go | 5 +++-- 6 files changed, 19 insertions(+), 5 deletions(-) create mode 100644 internal/resources/icon.go create mode 100644 internal/resources/icons/logo.png diff --git a/README.md b/README.md index 38031b4..23180b8 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,6 @@ * fyne-cross linux --icon icon.png --app-id "." -name "gui-for-ffmpeg" 7. Создаться папка **fyne-cross/bin** и там будет созданна папка с тем названием под которую Вы компилировали приложения (linux-amd64 или windows-amd64). 8. В папку **fyne-cross/bin/linux-amd64** или **fyne-cross/bin/windows-amd64** копируете: - * icon.png * LICENSE * LICENSE-3RD-PARTY.txt

Структура должна получиться такая:

diff --git a/images/screenshot-folder-structure.png b/images/screenshot-folder-structure.png index 58b4406fd5e0cefedcaa3999969c49cc8d4b901c..ab90c3614a9a8674697378a8b359abc1f4847895 100644 GIT binary patch delta 5587 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 diff --git a/internal/resources/icon.go b/internal/resources/icon.go new file mode 100644 index 0000000..003278e --- /dev/null +++ b/internal/resources/icon.go @@ -0,0 +1,13 @@ +package resources + +import ( + _ "embed" + "fyne.io/fyne/v2" +) + +//go:embed icons/logo.png +var iconAppLogo []byte + +func IconAppLogoResource() *fyne.StaticResource { + return fyne.NewStaticResource("icon.png", iconAppLogo) +} diff --git a/internal/resources/icons/logo.png b/internal/resources/icons/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..6eaf8f62869d7c2c6d0313a8f11557fdb18cfee2 GIT binary patch literal 29642 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 de Date: Fri, 6 Jun 2025 14:50:16 +0500 Subject: [PATCH 04/41] Refactor application structure and initialize core components I decided to rewrite the program taking into account the experience gained. --- internal/application/app.go | 51 +++++++ internal/application/progressbar.go | 63 ++++++++ internal/application/setting/ffmpeg.go | 25 +++ internal/application/setting/lang.go | 66 ++++++++ internal/application/setting/setting.go | 65 ++++++++ internal/controller/convertor.go | 8 + internal/controller/error.go | 16 ++ internal/controller/main.go | 64 ++++++++ internal/ffmpeg/ffmpeg.go | 19 +++ internal/ffmpeg/ffplay.go | 19 +++ internal/ffmpeg/ffprobe.go | 19 +++ internal/ffmpeg/utilities.go | 48 ++++++ internal/gui/view/convertor.go | 33 ++++ internal/gui/view/error.go | 39 +++++ .../gui/view/start_without_support_lang.go | 28 ++++ internal/gui/window/main.go | 140 +++++++++++++++++ internal/resources/translation.go | 12 ++ internal/resources/translations/app.en.json | 143 ++++++++++++++++++ internal/resources/translations/app.kk.json | 143 ++++++++++++++++++ internal/resources/translations/app.ru.json | 143 ++++++++++++++++++ internal/resources/translations/base.en.json | 45 ++++++ internal/resources/translations/base.kk.json | 45 ++++++ internal/resources/translations/base.ru.json | 45 ++++++ main.go | 79 ++-------- 24 files changed, 1295 insertions(+), 63 deletions(-) create mode 100644 internal/application/app.go create mode 100644 internal/application/progressbar.go create mode 100644 internal/application/setting/ffmpeg.go create mode 100644 internal/application/setting/lang.go create mode 100644 internal/application/setting/setting.go create mode 100644 internal/controller/convertor.go create mode 100644 internal/controller/error.go create mode 100644 internal/controller/main.go create mode 100644 internal/ffmpeg/ffmpeg.go create mode 100644 internal/ffmpeg/ffplay.go create mode 100644 internal/ffmpeg/ffprobe.go create mode 100644 internal/ffmpeg/utilities.go create mode 100644 internal/gui/view/convertor.go create mode 100644 internal/gui/view/error.go create mode 100644 internal/gui/view/start_without_support_lang.go create mode 100644 internal/gui/window/main.go create mode 100644 internal/resources/translation.go create mode 100644 internal/resources/translations/app.en.json create mode 100644 internal/resources/translations/app.kk.json create mode 100644 internal/resources/translations/app.ru.json create mode 100644 internal/resources/translations/base.en.json create mode 100644 internal/resources/translations/base.kk.json create mode 100644 internal/resources/translations/base.ru.json diff --git a/internal/application/app.go b/internal/application/app.go new file mode 100644 index 0000000..128c088 --- /dev/null +++ b/internal/application/app.go @@ -0,0 +1,51 @@ +package application + +import ( + "fyne.io/fyne/v2" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application/setting" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg" +) + +type AppContract interface { + FyneApp() fyne.App + GetSetting() setting.SettingContract + GetProgressBarService() ProgressBarContract + GetFFmpegService() ffmpeg.UtilitiesContract + Run() +} + +type application struct { + fyneApp fyne.App + setting setting.SettingContract + progressBarService ProgressBarContract + ffmpegService ffmpeg.UtilitiesContract +} + +func NewApp(fyneApp fyne.App, setting setting.SettingContract, progressBarService ProgressBarContract, ffmpegService ffmpeg.UtilitiesContract) AppContract { + return &application{ + fyneApp: fyneApp, + setting: setting, + progressBarService: progressBarService, + ffmpegService: ffmpegService, + } +} + +func (a *application) FyneApp() fyne.App { + return a.fyneApp +} + +func (a *application) GetSetting() setting.SettingContract { + return a.setting +} + +func (a *application) GetProgressBarService() ProgressBarContract { + return a.progressBarService +} + +func (a *application) GetFFmpegService() ffmpeg.UtilitiesContract { + return a.ffmpegService +} + +func (a *application) Run() { + a.fyneApp.Run() +} diff --git a/internal/application/progressbar.go b/internal/application/progressbar.go new file mode 100644 index 0000000..8890088 --- /dev/null +++ b/internal/application/progressbar.go @@ -0,0 +1,63 @@ +package application + +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" +) + +type ProgressBarContract interface { + GetContainer() *fyne.Container +} + +type progressBar struct { + container *fyne.Container + label *widget.Label + progressbar *widget.ProgressBar + errorBlock *container.Scroll + messageError *canvas.Text + statusMessage *canvas.Text + buttonPlay *widget.Button +} + +func NewProgressBar() ProgressBarContract { + 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 &progressBar{ + container: content, + label: label, + progressbar: progressbar, + errorBlock: errorBlock, + messageError: messageError, + statusMessage: statusMessage, + buttonPlay: buttonPlay, + } +} + +func (p *progressBar) GetContainer() *fyne.Container { + return p.container +} diff --git a/internal/application/setting/ffmpeg.go b/internal/application/setting/ffmpeg.go new file mode 100644 index 0000000..84f0789 --- /dev/null +++ b/internal/application/setting/ffmpeg.go @@ -0,0 +1,25 @@ +package setting + +func (s *setting) GetFFmpegPath() string { + return s.fyneApp.Preferences().String("ffmpegPath") +} + +func (s *setting) SetFFmpegPath(path string) { + s.fyneApp.Preferences().SetString("ffmpegPath", path) +} + +func (s *setting) GetFFprobePath() string { + return s.fyneApp.Preferences().String("ffprobePath") +} + +func (s *setting) SetFFprobePath(path string) { + s.fyneApp.Preferences().SetString("ffprobePath", path) +} + +func (s *setting) GetFFplayPath() string { + return s.fyneApp.Preferences().String("ffplayPath") +} + +func (s *setting) SetFFplayPath(path string) { + s.fyneApp.Preferences().SetString("ffplayPath", path) +} diff --git a/internal/application/setting/lang.go b/internal/application/setting/lang.go new file mode 100644 index 0000000..f7bf953 --- /dev/null +++ b/internal/application/setting/lang.go @@ -0,0 +1,66 @@ +package setting + +import ( + "encoding/json" + "fyne.io/fyne/v2" + fyneLang "fyne.io/fyne/v2/lang" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/resources" +) + +var supportedLanguages = map[string]Lang{ + "ru": {Code: "ru", Title: "Русский"}, + "kk": {Code: "kk", Title: "Қазақ Тілі"}, + "en": {Code: "en", Title: "English"}, +} + +type Lang struct { + Code string + Title string +} + +func ChangeLang(lang Lang) error { + translationsData, err := getTranslations(lang) + if err != nil { + return err + } + + name := fyneLang.SystemLocale().LanguageString() + return fyneLang.AddTranslations(fyne.NewStaticResource(name+".json", translationsData)) +} + +func defaultLang() Lang { + return supportedLanguages["ru"] +} + +func getTranslations(language Lang) ([]byte, error) { + translations := resources.GetTranslations() + + baseJson, err := translations.ReadFile("translations/base." + language.Code + ".json") + if err != nil { + return nil, err + } + appJson, err := translations.ReadFile("translations/app." + language.Code + ".json") + if err != nil { + return nil, err + } + + return mergeTranslations(baseJson, appJson) +} + +func mergeTranslations(baseJson []byte, appJson []byte) ([]byte, error) { + base := map[string]interface{}{} + custom := map[string]interface{}{} + err := json.Unmarshal(baseJson, &base) + if err != nil { + return nil, err + } + err = json.Unmarshal(appJson, &custom) + if err != nil { + return nil, err + } + + for k, v := range custom { + base[k] = v + } + return json.Marshal(base) +} diff --git a/internal/application/setting/setting.go b/internal/application/setting/setting.go new file mode 100644 index 0000000..d10d324 --- /dev/null +++ b/internal/application/setting/setting.go @@ -0,0 +1,65 @@ +package setting + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/lang" + "golang.org/x/text/language" +) + +type SettingContract interface { + GetLanguages() []Lang + GetCurrentLangOrDefaultLang() (currentLang Lang, isDefault bool) + SetLang(language Lang) error + GetFFmpegPath() string + SetFFmpegPath(path string) + GetFFprobePath() string + SetFFprobePath(path string) + GetFFplayPath() string + SetFFplayPath(path string) +} + +type setting struct { + fyneApp fyne.App +} + +func NewSetting(fyneApp fyne.App) SettingContract { + return &setting{ + fyneApp: fyneApp, + } +} + +func (s *setting) GetLanguages() []Lang { + items := []Lang{} + for _, item := range supportedLanguages { + items = append(items, item) + } + return items +} + +func (s *setting) GetCurrentLangOrDefaultLang() (currentLang Lang, isDefault bool) { + languageCode := s.fyneApp.Preferences().String("language") + + if languageCode == "" { + languageTag, err := language.Parse(lang.SystemLocale().LanguageString()) + if err != nil { + return currentLang, true + } + base, _ := languageTag.Base() + languageCode = base.String() + } + + if currentLang, ok := supportedLanguages[languageCode]; ok { + return currentLang, false + } + + return defaultLang(), true +} + +func (s *setting) SetLang(language Lang) error { + err := ChangeLang(language) + if err != nil { + return err + } + s.fyneApp.Preferences().SetString("language", language.Code) + return nil +} diff --git a/internal/controller/convertor.go b/internal/controller/convertor.go new file mode 100644 index 0000000..1b6ccdc --- /dev/null +++ b/internal/controller/convertor.go @@ -0,0 +1,8 @@ +package controller + +import "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/gui/view" + +func (c *controller) convertor() { + content := view.Convertor() + c.window.SetContent(content) +} diff --git a/internal/controller/error.go b/internal/controller/error.go new file mode 100644 index 0000000..0f66887 --- /dev/null +++ b/internal/controller/error.go @@ -0,0 +1,16 @@ +package controller + +import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application/setting" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/gui/view" +) + +func (c *controller) startWithError(err error) { + languages := c.app.GetSetting().GetLanguages() + + content := view.StartWithError(err, languages, func(lang setting.Lang) { + _ = setting.ChangeLang(lang) + c.startWithError(err) + }) + c.window.SetContent(content) +} diff --git a/internal/controller/main.go b/internal/controller/main.go new file mode 100644 index 0000000..2e5832a --- /dev/null +++ b/internal/controller/main.go @@ -0,0 +1,64 @@ +package controller + +import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application/setting" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/gui/view" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/gui/window" +) + +type ControllerContract interface { + Start() +} + +type controller struct { + app application.AppContract + window window.MainWindowContract +} + +func NewController(app application.AppContract) ControllerContract { + fyneWindow := app.FyneApp().NewWindow(app.FyneApp().Metadata().Name) + + return &controller{ + app: app, + window: window.NewMainWindow(fyneWindow, app.GetProgressBarService()), + } +} + +func (c *controller) Start() { + c.window.Show() + + isDefault, err := c.initLanguage() + if err != nil { + c.startWithError(err) + return + } + + if isDefault { + languages := c.app.GetSetting().GetLanguages() + content := view.StartWithoutSupportLang(languages, func(lang setting.Lang) { + err = c.app.GetSetting().SetLang(lang) + if err != nil { + c.startWithError(err) + return + } + c.window.InitLayout() + c.verificareaFFmpeg() + }) + c.window.SetContent(content) + return + } + + c.window.InitLayout() + c.verificareaFFmpeg() +} + +func (c *controller) verificareaFFmpeg() { + c.convertor() +} + +func (c *controller) initLanguage() (isDefault bool, err error) { + lang, isDefault := c.app.GetSetting().GetCurrentLangOrDefaultLang() + err = setting.ChangeLang(lang) + return isDefault, err +} diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go new file mode 100644 index 0000000..c7e3019 --- /dev/null +++ b/internal/ffmpeg/ffmpeg.go @@ -0,0 +1,19 @@ +package ffmpeg + +type FFmpegContract interface { + SetPath(path string) +} + +type ffmpeg struct { + path string +} + +func newFFmpeg(path string) FFmpegContract { + return &ffmpeg{ + path: path, + } +} + +func (f *ffmpeg) SetPath(path string) { + f.path = path +} diff --git a/internal/ffmpeg/ffplay.go b/internal/ffmpeg/ffplay.go new file mode 100644 index 0000000..105359f --- /dev/null +++ b/internal/ffmpeg/ffplay.go @@ -0,0 +1,19 @@ +package ffmpeg + +type FFplayContract interface { + SetPath(path string) +} + +type ffplay struct { + path string +} + +func newFFplay(path string) FFplayContract { + return &ffplay{ + path: path, + } +} + +func (f *ffplay) SetPath(path string) { + f.path = path +} diff --git a/internal/ffmpeg/ffprobe.go b/internal/ffmpeg/ffprobe.go new file mode 100644 index 0000000..ee8d9c8 --- /dev/null +++ b/internal/ffmpeg/ffprobe.go @@ -0,0 +1,19 @@ +package ffmpeg + +type FFprobeContract interface { + SetPath(path string) +} + +type ffprobe struct { + path string +} + +func newFFprobe(path string) FFprobeContract { + return &ffprobe{ + path: path, + } +} + +func (f *ffprobe) SetPath(path string) { + f.path = path +} diff --git a/internal/ffmpeg/utilities.go b/internal/ffmpeg/utilities.go new file mode 100644 index 0000000..309019d --- /dev/null +++ b/internal/ffmpeg/utilities.go @@ -0,0 +1,48 @@ +package ffmpeg + +import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application/setting" +) + +type UtilitiesContract interface { + GetFFmpeg() FFmpegContract + GetFFprobe() FFprobeContract + GetFFplay() FFplayContract +} + +type utilities struct { + setting setting.SettingContract + ffmpeg FFmpegContract + ffprobe FFprobeContract + ffplay FFplayContract +} + +func NewUtilities(setting setting.SettingContract) UtilitiesContract { + return &utilities{ + setting: setting, + } +} + +func (u *utilities) GetFFmpeg() FFmpegContract { + if u.ffmpeg == nil { + u.ffmpeg = newFFmpeg(u.setting.GetFFmpegPath()) + } + + return u.ffmpeg +} + +func (u *utilities) GetFFprobe() FFprobeContract { + if u.ffprobe != nil { + u.ffprobe = newFFprobe(u.setting.GetFFprobePath()) + } + + return u.ffprobe +} + +func (u *utilities) GetFFplay() FFplayContract { + if u.ffplay == nil { + u.ffplay = newFFplay(u.setting.GetFFplayPath()) + } + + return u.ffplay +} diff --git a/internal/gui/view/convertor.go b/internal/gui/view/convertor.go new file mode 100644 index 0000000..db11d16 --- /dev/null +++ b/internal/gui/view/convertor.go @@ -0,0 +1,33 @@ +package view + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/lang" + "fyne.io/fyne/v2/widget" +) + +func Convertor() fyne.CanvasObject { + form := newFormConvertor() + + converterVideoFilesTitle := lang.L("converterVideoFilesTitle") + return widget.NewCard(converterVideoFilesTitle, "", container.NewVScroll(form.getForm())) +} + +type formConvertor struct { + form *widget.Form + items []*widget.FormItem +} + +func newFormConvertor() *formConvertor { + f := widget.NewForm() + f.SubmitText = lang.L("converterVideoFilesSubmitTitle") + + return &formConvertor{ + form: f, + } +} + +func (f *formConvertor) getForm() *widget.Form { + return f.form +} diff --git a/internal/gui/view/error.go b/internal/gui/view/error.go new file mode 100644 index 0000000..f1860e3 --- /dev/null +++ b/internal/gui/view/error.go @@ -0,0 +1,39 @@ +package view + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/lang" + "fyne.io/fyne/v2/widget" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application/setting" +) + +func StartWithError(err error, languages []setting.Lang, funcSelected func(lang setting.Lang)) fyne.CanvasObject { + messageHead := lang.L("error") + + listView := widget.NewList( + func() int { + return len(languages) + }, + func() fyne.CanvasObject { + return widget.NewLabel("template") + }, + func(i widget.ListItemID, o fyne.CanvasObject) { + block := o.(*widget.Label) + block.SetText(languages[i].Title) + }) + listView.OnSelected = func(id widget.ListItemID) { + funcSelected(languages[id]) + } + + return container.NewBorder( + container.NewVBox( + widget.NewLabel(messageHead), + widget.NewLabel(err.Error()), + ), + nil, + nil, + nil, + listView, + ) +} diff --git a/internal/gui/view/start_without_support_lang.go b/internal/gui/view/start_without_support_lang.go new file mode 100644 index 0000000..6d9a3e3 --- /dev/null +++ b/internal/gui/view/start_without_support_lang.go @@ -0,0 +1,28 @@ +package view + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/lang" + "fyne.io/fyne/v2/widget" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application/setting" +) + +func StartWithoutSupportLang(languages []setting.Lang, funcSelected func(lang setting.Lang)) fyne.CanvasObject { + listView := widget.NewList( + func() int { + return len(languages) + }, + func() fyne.CanvasObject { + return widget.NewLabel("template") + }, + func(i widget.ListItemID, o fyne.CanvasObject) { + block := o.(*widget.Label) + block.SetText(languages[i].Title) + }) + listView.OnSelected = func(id widget.ListItemID) { + funcSelected(languages[id]) + } + + messageHead := lang.L("languageSelectionHead") + return widget.NewCard(messageHead, "", listView) +} diff --git a/internal/gui/window/main.go b/internal/gui/window/main.go new file mode 100644 index 0000000..77f8e51 --- /dev/null +++ b/internal/gui/window/main.go @@ -0,0 +1,140 @@ +package window + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/lang" + "fyne.io/fyne/v2/widget" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application" +) + +type MainWindowContract interface { + SetContent(content fyne.CanvasObject) + Show() + InitLayout() +} + +type mainWindow struct { + fyneWindow fyne.Window + layout *fyne.Container + progressBarService application.ProgressBarContract +} + +func NewMainWindow(fyneWindow fyne.Window, progressBarService application.ProgressBarContract) MainWindowContract { + fyneWindow.Resize(fyne.Size{Width: 1039, Height: 599}) + fyneWindow.CenterOnScreen() + + return &mainWindow{ + fyneWindow: fyneWindow, + progressBarService: progressBarService, + } +} + +func (w *mainWindow) InitLayout() { + fyne.Do(func() { + rContainer := newRightContainer(w.progressBarService.GetContainer()) + layout := container.NewAdaptiveGrid(2, widget.NewLabel(""), rContainer.GetCanvasObject()) + w.fyneWindow.SetContent(layout) + + w.layout = layout + }) +} + +func (w *mainWindow) SetContent(content fyne.CanvasObject) { + fyne.Do(func() { + if w.layout == nil { + w.fyneWindow.SetContent(content) + return + } + + w.layout.Objects[0] = content + w.fyneWindow.SetContent(w.layout) + }) +} + +func (w *mainWindow) Show() { + w.fyneWindow.Show() +} + +type RightMainContainerContract interface { + GetCanvasObject() fyne.CanvasObject + GetTabs() *container.AppTabs + GetAddedFilesContainer() *fyne.Container + GetFileQueueContainer() *fyne.Container + SelectFileQueueTab() + SelectAddedFilesTab() +} + +type rightMainContainer struct { + container fyne.CanvasObject + + tabs *container.AppTabs + + addedFilesContainer *fyne.Container + addedFilesTab *container.TabItem + + fileQueueContainer *fyne.Container + fileQueueTab *container.TabItem +} + +func newRightContainer(blockProgressbar *fyne.Container) RightMainContainerContract { + + addedFilesContainer := container.NewVBox() + addedFilesTab := container.NewTabItem(lang.L("addedFilesTitle"), addedFilesContainer) + + fileQueueContainer := container.NewVBox() + fileQueueTab := container.NewTabItem(lang.L("fileQueueTitle"), fileQueueContainer) + + tabs := container.NewAppTabs( + addedFilesTab, + fileQueueTab, + ) + + rightContainer := container.NewBorder( + container.NewVBox( + blockProgressbar, + widget.NewSeparator(), + ), + nil, + nil, + nil, + tabs, + ) + + return &rightMainContainer{ + container: rightContainer, + tabs: tabs, + addedFilesContainer: addedFilesContainer, + addedFilesTab: addedFilesTab, + fileQueueContainer: fileQueueContainer, + fileQueueTab: fileQueueTab, + } +} + +func (r *rightMainContainer) GetCanvasObject() fyne.CanvasObject { + return r.container +} + +func (r *rightMainContainer) GetTabs() *container.AppTabs { + return r.tabs +} + +func (r *rightMainContainer) GetAddedFilesContainer() *fyne.Container { + return r.addedFilesContainer +} + +func (r *rightMainContainer) GetFileQueueContainer() *fyne.Container { + return r.fileQueueContainer +} + +func (r *rightMainContainer) SelectFileQueueTab() { + fyne.Do(func() { + r.tabs.Select(r.fileQueueTab) + }) +} + +func (r *rightMainContainer) SelectAddedFilesTab() { + fyne.Do(func() { + r.tabs.Select(r.addedFilesTab) + }) +} diff --git a/internal/resources/translation.go b/internal/resources/translation.go new file mode 100644 index 0000000..401a291 --- /dev/null +++ b/internal/resources/translation.go @@ -0,0 +1,12 @@ +package resources + +import ( + "embed" +) + +//go:embed translations +var translations embed.FS + +func GetTranslations() embed.FS { + return translations +} diff --git a/internal/resources/translations/app.en.json b/internal/resources/translations/app.en.json new file mode 100644 index 0000000..9919f8d --- /dev/null +++ b/internal/resources/translations/app.en.json @@ -0,0 +1,143 @@ +{ + "AlsoUsedProgram": "The program also uses:", + "about": "About", + "aboutText": "A simple interface for the FFmpeg console utility. \nBut I am not the author of the FFmpeg utility itself.", + "addedFilesTitle": "Added files", + "autoClearAfterAddingToQueue": "Auto-clear after adding to queue", + "buttonDownloadFFmpeg": "Download FFmpeg automatically", + "buttonForSelectedDirTitle": "Save to folder:", + "cancel": "Cancel", + "changeFFPath": "FFmpeg, FFprobe and FFplay", + "changeLanguage": "Change language", + "checkboxOverwriteOutputFilesTitle": "Allow file to be overwritten", + "choose": "choose", + "clearAll": "Clear List", + "completedQueue": "Completed", + "converterVideoFilesSubmitTitle": "Convert", + "converterVideoFilesTitle": "Video, audio and picture converter", + "download": "Download", + "downloadFFmpegFromSite": "Will be downloaded from the site:", + "downloadRun": "Downloading...", + "dragAndDropFiles": "drag and drop files", + "encoderGroupAudio": "Audio", + "encoderGroupImage": "Images", + "encoderGroupVideo": "Video", + "encoder_apng": "APNG image", + "encoder_bmp": "BMP image", + "encoder_flv": "FLV", + "encoder_gif": "GIF image", + "encoder_h264_nvenc": "H.264 with NVIDIA support", + "encoder_libmp3lame": "libmp3lame MP3 (MPEG audio layer 3)", + "encoder_libshine": "libshine MP3 (MPEG audio layer 3)", + "encoder_libtwolame": "libtwolame MP2 (MPEG audio layer 2)", + "encoder_libvpx": "libvpx VP8 (codec vp8)", + "encoder_libvpx-vp9": "libvpx VP9 (codec vp9)", + "encoder_libwebp": "libwebp WebP image", + "encoder_libwebp_anim": "libwebp_anim WebP image", + "encoder_libx264": "H.264 libx264", + "encoder_libx265": "H.265 libx265", + "encoder_libxvid": "libxvidcore MPEG-4 part 2", + "encoder_mjpeg": "MJPEG (Motion JPEG)", + "encoder_mp2": "MP2 (MPEG audio layer 2)", + "encoder_mp2fixed": "MP2 fixed point (MPEG audio layer 2)", + "encoder_mpeg1video": "MPEG-1", + "encoder_mpeg2video": "MPEG-2", + "encoder_mpeg4": "MPEG-4 part 2", + "encoder_msmpeg4": "MPEG-4 part 2 Microsoft variant version 3", + "encoder_msmpeg4v2": "MPEG-4 part 2 Microsoft variant version 2", + "encoder_msvideo1": "Microsoft Video-1", + "encoder_png": "PNG image", + "encoder_qtrle": "QuickTime Animation (RLE) video", + "encoder_sgi": "SGI image", + "encoder_tiff": "TIFF image", + "encoder_wmav1": "Windows Media Audio 1", + "encoder_wmav2": "Windows Media Audio 2", + "encoder_wmv1": "Windows Media Video 7", + "encoder_wmv2": "Windows Media Video 8", + "encoder_xbm": "XBM (X BitMap) image", + "error": "An error has occurred!", + "errorConverter": "Couldn't convert video", + "errorDragAndDropFile": "Not all files were added", + "errorFFmpeg": "this is not FFmpeg", + "errorFFmpegVersion": "Could not determine FFmpeg version", + "errorFFplay": "this is not FFplay", + "errorFFplayVersion": "Could not determine FFplay version", + "errorFFprobe": "this is not FFprobe", + "errorFFprobeVersion": "Failed to determine FFprobe version", + "errorNoFilesAddedForConversion": "There are no files to convert", + "errorQueue": "Error", + "errorSelectedEncoder": "Converter not selected", + "errorSelectedFolderSave": "No save folder selected!", + "errorSelectedFormat": "File extension not selected", + "exit": "Exit", + "ffmpegLGPL": "This software uses libraries from the **FFmpeg** project under the **[LGPLv2.1](http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html)**.", + "ffmpegTrademark": "**FFmpeg** is a trademark of **[Fabrice Bellard](http://bellard.org/)**, originator of the **[FFmpeg](https://ffmpeg.org/about.html)** project.", + "fileForConversionTitle": "File:", + "fileQueueTitle": "Queue", + "formPreset": "Preset", + "gratitude": "Gratitude", + "gratitudeText": "I sincerely thank you for your invaluable\n\r and timely assistance:", + "help": "Help", + "helpFFplay": "FFplay Player Keys", + "helpFFplayActivateFrameStepMode": "Activate frame-by-frame mode.", + "helpFFplayCycleVideoFiltersOrShowModes": "A cycle of video filters or display modes.", + "helpFFplayDecreaseVolume": "Decrease the volume.", + "helpFFplayDescription": "Description", + "helpFFplayDoubleClickLeftMouseButton": "double click\nleft mouse button", + "helpFFplayIncreaseVolume": "Increase the volume.", + "helpFFplayKeyDown": "down", + "helpFFplayKeyHoldS": "hold S", + "helpFFplayKeyLeft": "left", + "helpFFplayKeyRight": "right", + "helpFFplayKeySpace": "SPACE", + "helpFFplayKeyUp": "up", + "helpFFplayKeys": "Keys", + "helpFFplayPause": "Pause or continue playing.", + "helpFFplayQuit": "Close the player.", + "helpFFplaySeekBForward10Minutes": "Fast forward 10 minutes.", + "helpFFplaySeekBForward1Minute": "Fast forward 1 minute.", + "helpFFplaySeekBackward10Minutes": "Rewind 10 minutes.", + "helpFFplaySeekBackward10Seconds": "Rewind 10 seconds.", + "helpFFplaySeekBackward1Minute": "Rewind 1 minute.", + "helpFFplaySeekForward10Seconds": "Fast forward 10 seconds.", + "helpFFplayToggleFullScreen": "Switch to full screen or exit full screen.", + "helpFFplayToggleMute": "Mute or unmute.", + "inProgressQueue": "In Progress", + "languageSelectionFormHead": "Switch language", + "languageSelectionHead": "Choose language", + "licenseLink": "License information", + "licenseLinkOther": "Licenses from other products used in the program", + "menuSettingsLanguage": "Language", + "menuSettingsTheme": "Theme", + "or": "or", + "parameterCheckbox": "Enable option", + "pathToFfmpeg": "Path to FFmpeg:", + "pathToFfplay": "Path to FFplay:", + "pathToFfprobe": "Path to FFprobe:", + "preset_fast": "fast (slower than \"faster\", but the file will weigh less)", + "preset_faster": "faster (slower than \"veryfast\", but the file will weigh less)", + "preset_medium": "medium (slower than \"fast\", but the file will weigh less)", + "preset_placebo": "placebo (not recommended)", + "preset_slow": "slow (slower than \"medium\", but the file will weigh less)", + "preset_slower": "slower (slower than \"slow\", but the file will weigh less)", + "preset_superfast": "superfast (slower than \"ultrafast\", but the file will weigh less)", + "preset_ultrafast": "ultrafast (fast, but the file will weigh a lot)", + "preset_veryfast": "veryfast (slower than \"superfast\", but the file will weigh less)", + "preset_veryslow": "veryslow (slower than \"slower\", but the file will weigh less)", + "programmLink": "Project website", + "programmVersion": "**Program version:** {{.Version}}", + "queue": "Queue", + "save": "Save", + "selectEncoder": "Encoder:", + "selectFFPathTitle": "Specify the path to FFmpeg and FFprobe", + "selectFormat": "File extension:", + "settings": "Settings", + "testFF": "Checking FFmpeg for serviceability...", + "themesNameDark": "Dark", + "themesNameDefault": "Default", + "themesNameLight": "Light", + "titleDownloadLink": "You can download it from here", + "total": "Total", + "unzipRun": "Unpacked...", + "waitingQueue": "Waiting" +} \ No newline at end of file diff --git a/internal/resources/translations/app.kk.json b/internal/resources/translations/app.kk.json new file mode 100644 index 0000000..6fb1b81 --- /dev/null +++ b/internal/resources/translations/app.kk.json @@ -0,0 +1,143 @@ +{ + "AlsoUsedProgram": "Бағдарлама сонымен қатар пайдаланады:", + "about": "Бағдарлама туралы", + "aboutText": "FFmpeg консоль утилитасы үшін қарапайым интерфейс. \nБірақ мен FFmpeg утилитасының авторы емеспін.", + "addedFilesTitle": "Қосылған файлдар", + "autoClearAfterAddingToQueue": "Кезекке қосқаннан кейін тазалаңыз", + "buttonDownloadFFmpeg": "FFmpeg автоматты түрде жүктеп алыңыз", + "buttonForSelectedDirTitle": "Қалтаға сақтаңыз:", + "cancel": "Болдырмау", + "changeFFPath": "FFmpeg, FFprobe және FFplay", + "changeLanguage": "Тілді өзгерту", + "checkboxOverwriteOutputFilesTitle": "Файлды қайта жазуға рұқсат беріңіз", + "choose": "таңдау", + "clearAll": "Тізімді өшіру", + "completedQueue": "Дайын", + "converterVideoFilesSubmitTitle": "Файлды түрлендіру", + "converterVideoFilesTitle": "Бейне, аудио және суретті түрлендіргіш", + "download": "Жүктеп алу", + "downloadFFmpegFromSite": "Сайттан жүктеледі:", + "downloadRun": "Жүктеп алынуда...", + "dragAndDropFiles": "файлдарды сүйреп апарыңыз", + "encoderGroupAudio": "Аудио", + "encoderGroupImage": "Суреттер", + "encoderGroupVideo": "Бейне", + "encoder_apng": "APNG image", + "encoder_bmp": "BMP image", + "encoder_flv": "FLV", + "encoder_gif": "GIF image", + "encoder_h264_nvenc": "NVIDIA қолдауымен H.264", + "encoder_libmp3lame": "libmp3lame MP3 (MPEG audio layer 3)", + "encoder_libshine": "libshine MP3 (MPEG audio layer 3)", + "encoder_libtwolame": "libtwolame MP2 (MPEG audio layer 2)", + "encoder_libvpx": "libvpx VP8 (codec vp8)", + "encoder_libvpx-vp9": "libvpx VP9 (codec vp9)", + "encoder_libwebp": "libwebp WebP image", + "encoder_libwebp_anim": "libwebp_anim WebP image", + "encoder_libx264": "H.264 libx264", + "encoder_libx265": "H.265 libx265", + "encoder_libxvid": "libxvidcore MPEG-4 part 2", + "encoder_mjpeg": "MJPEG (Motion JPEG)", + "encoder_mp2": "MP2 (MPEG audio layer 2)", + "encoder_mp2fixed": "MP2 fixed point (MPEG audio layer 2)", + "encoder_mpeg1video": "MPEG-1", + "encoder_mpeg2video": "MPEG-2", + "encoder_mpeg4": "MPEG-4 part 2", + "encoder_msmpeg4": "MPEG-4 part 2 Microsoft variant version 3", + "encoder_msmpeg4v2": "MPEG-4 part 2 Microsoft variant version 2", + "encoder_msvideo1": "Microsoft Video-1", + "encoder_png": "PNG image", + "encoder_qtrle": "QuickTime Animation (RLE) video", + "encoder_sgi": "SGI image", + "encoder_tiff": "TIFF image", + "encoder_wmav1": "Windows Media Audio 1", + "encoder_wmav2": "Windows Media Audio 2", + "encoder_wmv1": "Windows Media Video 7", + "encoder_wmv2": "Windows Media Video 8", + "encoder_xbm": "XBM (X BitMap) image", + "error": "Қате орын алды!", + "errorConverter": "Бейнені түрлендіру мүмкін болмады", + "errorDragAndDropFile": "Барлық файлдар қосылмаған", + "errorFFmpeg": "бұл FFmpeg емес", + "errorFFmpegVersion": "FFmpeg нұсқасын анықтау мүмкін болмады", + "errorFFplay": "бұл FFplay емес", + "errorFFplayVersion": "FFplay нұсқасын анықтау мүмкін болмады", + "errorFFprobe": "бұл FFprobe емес", + "errorFFprobeVersion": "FFprobe нұсқасын анықтау мүмкін болмады", + "errorNoFilesAddedForConversion": "Түрлендіруге арналған файлдар жоқ", + "errorQueue": "Қате", + "errorSelectedEncoder": "Түрлендіргіш таңдалмаған", + "errorSelectedFolderSave": "Сақтау қалтасы таңдалмаған!", + "errorSelectedFormat": "Файл кеңейтімі таңдалмаған", + "exit": "Шығу", + "ffmpegLGPL": "Бұл бағдарламалық құрал **[LGPLv2.1](http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html)** астында **FFmpeg** жобасының кітапханаларын пайдаланады.", + "ffmpegTrademark": "FFmpeg — **[FFmpeg](https://ffmpeg.org/about.html)** жобасын жасаушы **[Fabrice Bellard](http://bellard.org/)** сауда белгісі.", + "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» қарағанда баяуырақ, бірақ файлдың салмағы аз болады)", + "preset_medium": "medium («fast» қарағанда баяуырақ, бірақ файлдың салмағы аз болады)", + "preset_placebo": "placebo (ұсынылмайды)", + "preset_slow": "slow («medium» қарағанда баяуырақ, бірақ файлдың салмағы аз болады)", + "preset_slower": "slower («slow» қарағанда баяуырақ, бірақ файлдың салмағы аз болады)", + "preset_superfast": "superfast («ultrafast» қарағанда баяуырақ, бірақ файлдың салмағы аз болады)", + "preset_ultrafast": "ultrafast (жылдам, бірақ файлдың салмағы көп болады)", + "preset_veryfast": "veryfast («superfast» қарағанда баяуырақ, бірақ файлдың салмағы аз болады)", + "preset_veryslow": "veryslow («slower» қарағанда баяуырақ, бірақ файлдың салмағы аз болады)", + "programmLink": "Жобаның веб-сайты", + "programmVersion": "**Бағдарлама нұсқасы:** {{.Version}}", + "queue": "Кезек", + "save": "Сақтау", + "selectEncoder": "Кодировщик:", + "selectFFPathTitle": "FFmpeg және FFprobe жолын көрсетіңіз", + "selectFormat": "Файл кеңейтімі:", + "settings": "Параметрлер", + "testFF": "FFmpeg функционалдығы тексерілуде...", + "themesNameDark": "Қараңғы тақырып", + "themesNameDefault": "Әдепкі бойынша", + "themesNameLight": "Жеңіл тақырып", + "titleDownloadLink": "Сіз оны осы жерден жүктей аласыз", + "total": "Барлығы", + "unzipRun": "Орамнан шығарылуда...", + "waitingQueue": "Күту" +} \ No newline at end of file diff --git a/internal/resources/translations/app.ru.json b/internal/resources/translations/app.ru.json new file mode 100644 index 0000000..20a5da8 --- /dev/null +++ b/internal/resources/translations/app.ru.json @@ -0,0 +1,143 @@ +{ + "AlsoUsedProgram": "Также в программе используется:", + "about": "О программе", + "aboutText": "Простенький интерфейс для консольной утилиты FFmpeg. \nНо я не являюсь автором самой утилиты FFmpeg.", + "addedFilesTitle": "Добавленные файлы", + "autoClearAfterAddingToQueue": "Очищать после добавления в очередь", + "buttonDownloadFFmpeg": "Скачать автоматически FFmpeg", + "buttonForSelectedDirTitle": "Сохранить в папку:", + "cancel": "Отмена", + "changeFFPath": "FFmpeg, FFprobe и FFplay", + "changeLanguage": "Поменять язык", + "checkboxOverwriteOutputFilesTitle": "Разрешить перезаписать файл", + "choose": "выбрать", + "clearAll": "Очистить список", + "completedQueue": "Готово", + "converterVideoFilesSubmitTitle": "Конвертировать", + "converterVideoFilesTitle": "Конвертер видео, аудио и картинок", + "download": "Скачать", + "downloadFFmpegFromSite": "Будет скачано с сайта:", + "downloadRun": "Скачивается...", + "dragAndDropFiles": "перетащить файлы", + "encoderGroupAudio": "Аудио", + "encoderGroupImage": "Картинки", + "encoderGroupVideo": "Видео", + "encoder_apng": "APNG image", + "encoder_bmp": "BMP image", + "encoder_flv": "FLV", + "encoder_gif": "GIF image", + "encoder_h264_nvenc": "H.264 с поддержкой NVIDIA", + "encoder_libmp3lame": "libmp3lame MP3 (MPEG audio layer 3)", + "encoder_libshine": "libshine MP3 (MPEG audio layer 3)", + "encoder_libtwolame": "libtwolame MP2 (MPEG audio layer 2)", + "encoder_libvpx": "libvpx VP8 (codec vp8)", + "encoder_libvpx-vp9": "libvpx VP9 (codec vp9)", + "encoder_libwebp": "libwebp WebP image", + "encoder_libwebp_anim": "libwebp_anim WebP image", + "encoder_libx264": "H.264 libx264", + "encoder_libx265": "H.265 libx265", + "encoder_libxvid": "libxvidcore MPEG-4 part 2", + "encoder_mjpeg": "MJPEG (Motion JPEG)", + "encoder_mp2": "MP2 (MPEG audio layer 2)", + "encoder_mp2fixed": "MP2 fixed point (MPEG audio layer 2)", + "encoder_mpeg1video": "MPEG-1", + "encoder_mpeg2video": "MPEG-2", + "encoder_mpeg4": "MPEG-4 part 2", + "encoder_msmpeg4": "MPEG-4 part 2 Microsoft variant version 3", + "encoder_msmpeg4v2": "MPEG-4 part 2 Microsoft variant version 2", + "encoder_msvideo1": "Microsoft Video-1", + "encoder_png": "PNG image", + "encoder_qtrle": "QuickTime Animation (RLE) video", + "encoder_sgi": "SGI image", + "encoder_tiff": "TIFF image", + "encoder_wmav1": "Windows Media Audio 1", + "encoder_wmav2": "Windows Media Audio 2", + "encoder_wmv1": "Windows Media Video 7", + "encoder_wmv2": "Windows Media Video 8", + "encoder_xbm": "XBM (X BitMap) image", + "error": "Произошла ошибка!", + "errorConverter": "не смогли отконвертировать видео", + "errorDragAndDropFile": "Не все файлы добавились", + "errorFFmpeg": "это не FFmpeg", + "errorFFmpegVersion": "Не смогли определить версию FFmpeg", + "errorFFplay": "это не FFplay", + "errorFFplayVersion": "Не смогли определить версию FFplay", + "errorFFprobe": "это не FFprobe", + "errorFFprobeVersion": "Не смогли определить версию FFprobe", + "errorNoFilesAddedForConversion": "Нет файлов для конвертации", + "errorQueue": "Ошибка", + "errorSelectedEncoder": "Конвертер не выбран", + "errorSelectedFolderSave": "Папка для сохранения не выбрана!", + "errorSelectedFormat": "Расширение файла не выбрана", + "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, но будет файл и меньше весить)", + "preset_medium": "medium (медленней чем fast, но будет файл и меньше весить)", + "preset_placebo": "placebo (не рекомендуется)", + "preset_slow": "slow (медленней чем medium, но будет файл и меньше весить)", + "preset_slower": "slower (медленней чем slow, но будет файл и меньше весить)", + "preset_superfast": "superfast (медленней чем ultrafast, но будет файл и меньше весить)", + "preset_ultrafast": "ultrafast (быстро, но файл будет много весить)", + "preset_veryfast": "veryfast (медленней чем superfast, но будет файл и меньше весить)", + "preset_veryslow": "veryslow (медленней чем slower, но будет файл и меньше весить)", + "programmLink": "Сайт проекта", + "programmVersion": "**Версия программы:** {{.Version}}", + "queue": "Очередь", + "save": "Сохранить", + "selectEncoder": "Кодировщик:", + "selectFFPathTitle": "Укажите путь к FFmpeg и к FFprobe", + "selectFormat": "Расширение файла:", + "settings": "Настройки", + "testFF": "Проверка FFmpeg на работоспособность...", + "themesNameDark": "Тёмная", + "themesNameDefault": "По умолчанию", + "themesNameLight": "Светлая", + "titleDownloadLink": "Скачать можно от сюда", + "total": "Всего", + "unzipRun": "Распаковывается...", + "waitingQueue": "В очереди" +} \ No newline at end of file diff --git a/internal/resources/translations/base.en.json b/internal/resources/translations/base.en.json new file mode 100644 index 0000000..3f5dd3d --- /dev/null +++ b/internal/resources/translations/base.en.json @@ -0,0 +1,45 @@ +{ + "Advanced": "Advanced", + "Cancel": "Cancel", + "Confirm": "Confirm", + "Copy": "Copy", + "Create Folder": "Create Folder", + "Cut": "Cut", + "Enter filename": "Enter filename", + "Error": "Error", + "Favourites": "Favourites", + "File": "File", + "Folder": "Folder", + "New Folder": "New Folder", + "No": "No", + "OK": "OK", + "Open": "Open", + "Paste": "Paste", + "Quit": "Quit", + "Redo": "Redo", + "Save": "Save", + "Select all": "Select all", + "Show Hidden Files": "Show Hidden Files", + "Undo": "Undo", + "Yes": "Yes", + "file.name": { + "other": "Name" + }, + "file.parent": { + "other": "Parent" + }, + "friday": "Friday", + "friday.short": "Fri", + "monday": "Monday", + "monday.short": "Mon", + "saturday": "Saturday", + "saturday.short": "Sat", + "sunday": "Sunday", + "sunday.short": "Sun", + "thursday": "Thursday", + "thursday.short": "Thu", + "tuesday": "Tuesday", + "tuesday.short": "Tue", + "wednesday": "Wednesday", + "wednesday.short": "Wed" +} \ No newline at end of file diff --git a/internal/resources/translations/base.kk.json b/internal/resources/translations/base.kk.json new file mode 100644 index 0000000..469566b --- /dev/null +++ b/internal/resources/translations/base.kk.json @@ -0,0 +1,45 @@ +{ + "Advanced": "Кеңейтілген", + "Cancel": "Бас тарту", + "Confirm": "Растау", + "Copy": "Көшіру", + "Create Folder": "Қалта жасау", + "Cut": "Кесу", + "Enter filename": "Файл атауын енгізіңіз", + "Error": "Қате", + "Favourites": "Таңдаулылар", + "File": "Файл", + "Folder": "Қалта", + "New Folder": "Жаңа қалта", + "No": "Жоқ", + "OK": "ОК", + "Open": "Ашу", + "Paste": "Кірістіру", + "Quit": "Шығу", + "Redo": "Қайталау", + "Save": "Сақтау", + "Select all": "Барлығын таңдаңыз", + "Show Hidden Files": "Жасырын файлдарды көрсету", + "Undo": "Бас тарту", + "Yes": "Иә", + "file.name": { + "other": "Аты" + }, + "file.parent": { + "other": "Жоғары" + }, + "friday": "Жұма", + "friday.short": "Жұ", + "monday": "Дүйсенбі", + "monday.short": "Дү", + "saturday": "Сенбі", + "saturday.short": "Сен", + "sunday": "Жексенбі", + "sunday.short": "Же", + "thursday": "Сейсенбі", + "thursday.short": "Се", + "tuesday": "Бейсенбі", + "tuesday.short": "Бе", + "wednesday": "Сәрсенбі", + "wednesday.short": "Сә" +} \ No newline at end of file diff --git a/internal/resources/translations/base.ru.json b/internal/resources/translations/base.ru.json new file mode 100644 index 0000000..ac22058 --- /dev/null +++ b/internal/resources/translations/base.ru.json @@ -0,0 +1,45 @@ +{ + "Advanced": "Расширенные", + "Cancel": "Отмена", + "Confirm": "Подтвердить", + "Copy": "Копировать", + "Create Folder": "Создать папку", + "Cut": "Вырезать", + "Enter filename": "Введите имя файла", + "Error": "Ошибка", + "Favourites": "Избранное", + "File": "Файл", + "Folder": "Папка", + "New Folder": "Новая папка", + "No": "Нет", + "OK": "ОК", + "Open": "Открыть", + "Paste": "Вставить", + "Quit": "Выйти", + "Redo": "Повторить", + "Save": "Сохранить", + "Select all": "Выбрать всё", + "Show Hidden Files": "Показать скрытые файлы", + "Undo": "Отменить", + "Yes": "Да", + "file.name": { + "other": "Имя" + }, + "file.parent": { + "other": "Вверх" + }, + "friday": "Пятница", + "friday.short": "Пт", + "monday": "Понедельник", + "monday.short": "Пн", + "saturday": "Суббота", + "saturday.short": "Сб", + "sunday": "Воскресенье", + "sunday.short": "Вс", + "thursday": "Вторник", + "thursday.short": "Вт", + "tuesday": "Четверг", + "tuesday.short": "Чт", + "wednesday": "Среда", + "wednesday.short": "Ср" +} \ No newline at end of file diff --git a/main.go b/main.go index e299536..30a9ae7 100644 --- a/main.go +++ b/main.go @@ -1,76 +1,29 @@ package main import ( - _ "embed" "fyne.io/fyne/v2" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/convertor" - error2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/error" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/handler" + "fyne.io/fyne/v2/app" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application/setting" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/controller" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/resources" - "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/setting" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/theme" ) -var application kernel.AppContract -var ffPathUtilities *kernel.FFPathUtilities - -func init() { - appMetadata := &fyne.AppMetadata{ +func main() { + appMetadata := fyne.AppMetadata{ ID: "net.kor-elf.projects.gui-for-ffmpeg", Name: "GUI for FFmpeg", Version: "0.9.0", Icon: resources.IconAppLogoResource(), } - - ffPathUtilities = &kernel.FFPathUtilities{FFmpeg: "", FFprobe: "", FFplay: ""} - convertorService := kernel.NewService(ffPathUtilities) - ffplayService := kernel.NewFFplay(ffPathUtilities) - - queue := kernel.NewQueueList() - application = kernel.NewApp( - appMetadata, - queue, - ffplayService, - convertorService, - ) -} - -func main() { - errorView := error2.NewView(application) - - settingRepository := setting.NewRepository(application.GetAppFyne()) - settingDirectoryForSaving := setting.NewSettingDirectoryForSaving(settingRepository) - - convertorRepository := convertor.NewRepository(settingRepository) - ffPathUtilities.FFmpeg = convertorRepository.GetPathFfmpeg() - ffPathUtilities.FFprobe = convertorRepository.GetPathFfprobe() - ffPathUtilities.FFplay = convertorRepository.GetPathFfplay() - - application.RunConvertor() - defer application.AfterClosing() - - localizerView := localizer.NewView(application) - convertorView := convertor.NewView(application) - 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) - - menuView := menu.NewView(application) - menuSettingView := menu.NewViewSetting(application, themeService) - mainMenu := handler.NewMenuHandler(application, convertorHandler, menuView, menuSettingView, localizerView, themeService) - - mainHandler := handler.NewMainHandler(application, convertorHandler, mainMenu) - mainHandler.Start() - - application.GetWindow().SetMainMenu(mainMenu.GetMainMenu()) - application.GetWindow().ShowAndRun() + app.SetMetadata(appMetadata) + fyneApp := app.New() + progressBarService := application.NewProgressBar() + appSetting := setting.NewSetting(fyneApp) + ffmpegService := ffmpeg.NewUtilities(appSetting) + myApp := application.NewApp(fyneApp, appSetting, progressBarService, ffmpegService) + mainController := controller.NewController(myApp) + mainController.Start() + myApp.Run() } -- 2.47.2 From c60b9f7b0c859d425b35edaf8f7e9a115898cf98 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sat, 7 Jun 2025 01:30:32 +0500 Subject: [PATCH 05/41] Add FFmpeg utilities configuration UI and automated downloading Introduce a new UI for configuring FFmpeg, FFprobe, and FFplay paths with file selection and error handling. Add platform-specific logic for downloading and extracting FFmpeg binaries directly within the application, improving user experience. --- internal/application/setting/ffmpeg.go | 21 +- internal/controller/convertor.go | 52 +++- internal/controller/main.go | 12 +- .../ffmpeg/download/gui/download_anyos.go | 14 ++ .../ffmpeg/download/gui/download_linux.go | 59 +++++ .../ffmpeg/download/gui/download_windows.go | 59 +++++ internal/ffmpeg/download/service/download.go | 21 ++ .../ffmpeg/download/service/download_anyos.go | 15 ++ .../ffmpeg/download/service/download_linux.go | 236 ++++++++++++++++++ .../download/service/download_windows.go | 175 +++++++++++++ internal/ffmpeg/ffmpeg.go | 43 +++- internal/ffmpeg/ffplay.go | 43 +++- internal/ffmpeg/ffprobe.go | 43 +++- internal/ffmpeg/utilities.go | 143 ++++++++++- .../gui/view/configuring_ffmpeg_utilities.go | 129 ++++++++++ internal/gui/window/main.go | 17 +- internal/utils/dialog.go | 11 + internal/utils/path_separator.go | 8 + internal/utils/path_separator_window.go | 8 + internal/utils/prepare_background_command.go | 12 + .../prepare_background_command_windows.go | 13 + internal/utils/text.go | 21 ++ 22 files changed, 1118 insertions(+), 37 deletions(-) create mode 100644 internal/ffmpeg/download/gui/download_anyos.go create mode 100644 internal/ffmpeg/download/gui/download_linux.go create mode 100644 internal/ffmpeg/download/gui/download_windows.go create mode 100644 internal/ffmpeg/download/service/download.go create mode 100644 internal/ffmpeg/download/service/download_anyos.go create mode 100644 internal/ffmpeg/download/service/download_linux.go create mode 100644 internal/ffmpeg/download/service/download_windows.go create mode 100644 internal/gui/view/configuring_ffmpeg_utilities.go create mode 100644 internal/utils/dialog.go create mode 100644 internal/utils/path_separator.go create mode 100644 internal/utils/path_separator_window.go create mode 100644 internal/utils/prepare_background_command.go create mode 100644 internal/utils/prepare_background_command_windows.go create mode 100644 internal/utils/text.go diff --git a/internal/application/setting/ffmpeg.go b/internal/application/setting/ffmpeg.go index 84f0789..e20bb93 100644 --- a/internal/application/setting/ffmpeg.go +++ b/internal/application/setting/ffmpeg.go @@ -1,7 +1,12 @@ package setting func (s *setting) GetFFmpegPath() string { - return s.fyneApp.Preferences().String("ffmpegPath") + path := s.fyneApp.Preferences().String("ffmpegPath") + if path == "" { + return "ffmpeg" + } + + return path } func (s *setting) SetFFmpegPath(path string) { @@ -9,7 +14,12 @@ func (s *setting) SetFFmpegPath(path string) { } func (s *setting) GetFFprobePath() string { - return s.fyneApp.Preferences().String("ffprobePath") + path := s.fyneApp.Preferences().String("ffprobePath") + if path == "" { + return "ffprobe" + } + + return path } func (s *setting) SetFFprobePath(path string) { @@ -17,7 +27,12 @@ func (s *setting) SetFFprobePath(path string) { } func (s *setting) GetFFplayPath() string { - return s.fyneApp.Preferences().String("ffplayPath") + path := s.fyneApp.Preferences().String("ffplayPath") + if path == "" { + return "ffplay" + } + + return path } func (s *setting) SetFFplayPath(path string) { diff --git a/internal/controller/convertor.go b/internal/controller/convertor.go index 1b6ccdc..eba2f76 100644 --- a/internal/controller/convertor.go +++ b/internal/controller/convertor.go @@ -1,8 +1,58 @@ package controller -import "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/gui/view" +import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/download/service" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/gui/view" +) func (c *controller) convertor() { content := view.Convertor() c.window.SetContent(content) } + +func (c *controller) settingConvertor(isAllowCancellation bool) { + ffmpegPath := c.app.GetFFmpegService().GetFFmpegPath() + ffprobePath := c.app.GetFFmpegService().GetFFprobePath() + ffplayPath := c.app.GetFFmpegService().GetFFplayPath() + + var cancel func() + cancel = nil + if isAllowCancellation { + cancel = func() { + c.convertor() + } + } + + content := view.ConfiguringFFmpegUtilities( + c.window, + ffmpegPath, + ffprobePath, + ffplayPath, + c.saveSettingConvertor, + cancel, + service.DownloadFFmpeg(c.app, c.saveSettingConvertor), + ) + c.window.SetContent(content) +} + +func (c *controller) saveSettingConvertor(ffmpegPath string, ffprobePath string, ffplayPath string) error { + var err error + + err = c.app.GetFFmpegService().ChangeFFmpeg(ffmpegPath) + if err != nil { + return err + } + + c.app.GetFFmpegService().ChangeFFprobe(ffprobePath) + if err != nil { + return err + } + + c.app.GetFFmpegService().ChangeFFplay(ffplayPath) + if err != nil { + return err + } + + c.convertor() + return nil +} diff --git a/internal/controller/main.go b/internal/controller/main.go index 2e5832a..11cac23 100644 --- a/internal/controller/main.go +++ b/internal/controller/main.go @@ -13,7 +13,7 @@ type ControllerContract interface { type controller struct { app application.AppContract - window window.MainWindowContract + window window.WindowContract } func NewController(app application.AppContract) ControllerContract { @@ -26,11 +26,10 @@ func NewController(app application.AppContract) ControllerContract { } func (c *controller) Start() { - c.window.Show() - isDefault, err := c.initLanguage() if err != nil { c.startWithError(err) + c.window.Show() return } @@ -46,14 +45,21 @@ func (c *controller) Start() { c.verificareaFFmpeg() }) c.window.SetContent(content) + c.window.Show() return } c.window.InitLayout() c.verificareaFFmpeg() + c.window.Show() } func (c *controller) verificareaFFmpeg() { + if !c.app.GetFFmpegService().UtilityCheck() { + c.settingConvertor(false) + return + } + c.convertor() } diff --git a/internal/ffmpeg/download/gui/download_anyos.go b/internal/ffmpeg/download/gui/download_anyos.go new file mode 100644 index 0000000..6957f9d --- /dev/null +++ b/internal/ffmpeg/download/gui/download_anyos.go @@ -0,0 +1,14 @@ +//go:build !windows && !linux +// +build !windows,!linux + +package gui + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/widget" +) + +func DownloadFFmpeg(donwloadFFmpeg func(progressBar *widget.ProgressBar, progressMessage *canvas.Text) error) fyne.CanvasObject { + return container.NewVBox() +} diff --git a/internal/ffmpeg/download/gui/download_linux.go b/internal/ffmpeg/download/gui/download_linux.go new file mode 100644 index 0000000..4884408 --- /dev/null +++ b/internal/ffmpeg/download/gui/download_linux.go @@ -0,0 +1,59 @@ +//go:build linux +// +build linux + +package gui + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/lang" + "fyne.io/fyne/v2/widget" + "golang.org/x/image/colornames" + "image/color" +) + +func DownloadFFmpeg(donwloadFFmpeg func(progressBar *widget.ProgressBar, progressMessage *canvas.Text) error) fyne.CanvasObject { + errorDownloadFFmpegMessage := canvas.NewText("", color.RGBA{R: 255, G: 0, B: 0, A: 255}) + errorDownloadFFmpegMessage.TextSize = 16 + errorDownloadFFmpegMessage.TextStyle = fyne.TextStyle{Bold: true} + + progressDownloadFFmpegMessage := canvas.NewText("", color.RGBA{R: 49, G: 127, B: 114, A: 255}) + progressDownloadFFmpegMessage.TextSize = 16 + progressDownloadFFmpegMessage.TextStyle = fyne.TextStyle{Bold: true} + + progressBar := widget.NewProgressBar() + + var buttonDownloadFFmpeg *widget.Button + + buttonDownloadFFmpeg = widget.NewButton(lang.L("download"), func() { + fyne.Do(func() { + buttonDownloadFFmpeg.Disable() + }) + go func() { + err := donwloadFFmpeg(progressBar, progressDownloadFFmpegMessage) + if err != nil { + errorDownloadFFmpegMessage.Text = err.Error() + } + fyne.Do(func() { + buttonDownloadFFmpeg.Enable() + }) + }() + + }) + + downloadFFmpegFromSiteMessage := lang.L("downloadFFmpegFromSite") + + return container.NewVBox( + canvas.NewLine(colornames.Darkgreen), + widget.NewCard(lang.L("buttonDownloadFFmpeg"), "", container.NewVBox( + widget.NewRichTextFromMarkdown( + downloadFFmpegFromSiteMessage+" [https://github.com/BtbN/FFmpeg-Builds/releases](https://github.com/BtbN/FFmpeg-Builds/releases)", + ), + buttonDownloadFFmpeg, + container.NewHScroll(errorDownloadFFmpegMessage), + progressDownloadFFmpegMessage, + progressBar, + )), + ) +} diff --git a/internal/ffmpeg/download/gui/download_windows.go b/internal/ffmpeg/download/gui/download_windows.go new file mode 100644 index 0000000..1841311 --- /dev/null +++ b/internal/ffmpeg/download/gui/download_windows.go @@ -0,0 +1,59 @@ +//go:build windows +// +build windows + +package gui + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/lang" + "fyne.io/fyne/v2/widget" + "golang.org/x/image/colornames" + "image/color" +) + +func DownloadFFmpeg(donwloadFFmpeg func(progressBar *widget.ProgressBar, progressMessage *canvas.Text) error) fyne.CanvasObject { + errorDownloadFFmpegMessage := canvas.NewText("", color.RGBA{R: 255, G: 0, B: 0, A: 255}) + errorDownloadFFmpegMessage.TextSize = 16 + errorDownloadFFmpegMessage.TextStyle = fyne.TextStyle{Bold: true} + + progressDownloadFFmpegMessage := canvas.NewText("", color.RGBA{R: 49, G: 127, B: 114, A: 255}) + progressDownloadFFmpegMessage.TextSize = 16 + progressDownloadFFmpegMessage.TextStyle = fyne.TextStyle{Bold: true} + + progressBar := widget.NewProgressBar() + + var buttonDownloadFFmpeg *widget.Button + + buttonDownloadFFmpeg = widget.NewButton(lang.L("download"), func() { + + go func() { + fyne.Do(func() { + buttonDownloadFFmpeg.Disable() + }) + err := donwloadFFmpeg(progressBar, progressDownloadFFmpegMessage) + if err != nil { + errorDownloadFFmpegMessage.Text = err.Error() + } + fyne.Do(func() { + buttonDownloadFFmpeg.Enable() + }) + }() + }) + + downloadFFmpegFromSiteMessage := lang.L("downloadFFmpegFromSite") + + return container.NewVBox( + canvas.NewLine(colornames.Darkgreen), + widget.NewCard(lang.L("buttonDownloadFFmpeg"), "", container.NewVBox( + widget.NewRichTextFromMarkdown( + downloadFFmpegFromSiteMessage+" [https://github.com/BtbN/FFmpeg-Builds/releases](https://github.com/BtbN/FFmpeg-Builds/releases)", + ), + buttonDownloadFFmpeg, + container.NewHScroll(errorDownloadFFmpegMessage), + progressDownloadFFmpegMessage, + progressBar, + )), + ) +} diff --git a/internal/ffmpeg/download/service/download.go b/internal/ffmpeg/download/service/download.go new file mode 100644 index 0000000..f0a8438 --- /dev/null +++ b/internal/ffmpeg/download/service/download.go @@ -0,0 +1,21 @@ +package service + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/widget" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/download/gui" +) + +func DownloadFFmpeg(app application.AppContract, save func(ffmpegPath string, ffprobePath string, ffplayPath string) error) fyne.CanvasObject { + return gui.DownloadFFmpeg(func(progressBar *widget.ProgressBar, progressMessage *canvas.Text) error { + var err error + err = startDownload(app, progressBar, progressMessage, save) + if err != nil { + return err + } + + return nil + }) +} diff --git a/internal/ffmpeg/download/service/download_anyos.go b/internal/ffmpeg/download/service/download_anyos.go new file mode 100644 index 0000000..edee036 --- /dev/null +++ b/internal/ffmpeg/download/service/download_anyos.go @@ -0,0 +1,15 @@ +//go:build !windows && !linux +// +build !windows,!linux + +package service + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/widget" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application" +) + +func startDownload(app application.AppContract, progressBar *widget.ProgressBar, progressMessage *canvas.Text, save func(ffmpegPath string, ffprobePath string, ffplayPath string) error) error { + return nil +} diff --git a/internal/ffmpeg/download/service/download_linux.go b/internal/ffmpeg/download/service/download_linux.go new file mode 100644 index 0000000..32d0af0 --- /dev/null +++ b/internal/ffmpeg/download/service/download_linux.go @@ -0,0 +1,236 @@ +//go:build linux +// +build linux + +package service + +import ( + "archive/tar" + "errors" + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/lang" + "fyne.io/fyne/v2/widget" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application" + "github.com/ulikunitz/xz" + "io" + "net/http" + "os" + "path/filepath" +) + +func startDownload(app application.AppContract, progressBar *widget.ProgressBar, progressMessage *canvas.Text, save func(ffmpegPath string, ffprobePath string, ffplayPath string) error) error { + var err error + + dir, err := localSharePath() + if err != nil { + return err + } + dir = filepath.Join(dir, "fyne", app.FyneApp().UniqueID()) + err = os.MkdirAll(dir, 0755) + if err != nil { + return err + } + + fyne.Do(func() { + progressMessage.Text = lang.L("downloadRun") + progressMessage.Refresh() + }) + err = downloadFile(dir+"/ffmpeg.tar.xz", "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz", progressBar) + if err != nil { + return err + } + + fyne.Do(func() { + progressMessage.Text = lang.L("unzipRun") + progressMessage.Refresh() + }) + err = unTarXz(dir+"/ffmpeg.tar.xz", dir, progressBar) + if err != nil { + return err + } + _ = os.Remove(dir + "/ffmpeg.tar.xz") + + fyne.Do(func() { + progressMessage.Text = lang.L("testFF") + progressMessage.Refresh() + }) + + err = save( + dir+"/ffmpeg-master-latest-linux64-gpl/bin/ffmpeg", + dir+"/ffmpeg-master-latest-linux64-gpl/bin/ffprobe", + dir+"/ffmpeg-master-latest-linux64-gpl/bin/ffplay", + ) + if err != nil { + return err + } + + fyne.Do(func() { + progressMessage.Text = lang.L("completedQueue") + progressMessage.Refresh() + }) + + return nil +} + +func localSharePath() (string, error) { + xdgDataHome := os.Getenv("XDG_DATA_HOME") + if xdgDataHome != "" { + return xdgDataHome, nil + } + homeDir, err := os.UserHomeDir() + if err != nil { + return "", err + } + return filepath.Join(homeDir, ".local", "share"), nil +} + +func downloadFile(filepath string, url string, progressBar *widget.ProgressBar) (err error) { + progressBar.Value = 0 + progressBar.Max = 100 + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return err + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + f, err := os.OpenFile(filepath, os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return err + } + defer f.Close() + + buf := make([]byte, 32*1024) + var downloaded int64 + for { + n, err := resp.Body.Read(buf) + if err != nil { + if err == io.EOF { + break + } + return err + } + if n > 0 { + f.Write(buf[:n]) + downloaded += int64(n) + progressBar.Value = float64(downloaded) / float64(resp.ContentLength) * 100 + fyne.Do(func() { + progressBar.Refresh() + }) + } + } + return nil +} + +func unTarXz(fileTar string, directory string, progressBar *widget.ProgressBar) error { + progressBar.Value = 0 + progressBar.Max = 100 + + fyne.Do(func() { + progressBar.Refresh() + }) + + f, err := os.Open(fileTar) + if err != nil { + return err + } + defer f.Close() + + xzReader, err := xz.NewReader(f) + if err != nil { + return err + } + + tarReader := tar.NewReader(xzReader) + + totalFiles := 0 + for { + _, err := tarReader.Next() + if err == io.EOF { + break + } + if err != nil { + return err + } + totalFiles++ + } + + // Rewind back to the beginning of the file to re-process + _, err = f.Seek(0, 0) + if err != nil { + return err + } + + xzReader, err = xz.NewReader(f) + if err != nil { + return err + } + + tarReader = tar.NewReader(xzReader) + + // We count the number of files already unpacked + unpackedFiles := 0 + + for { + header, err := tarReader.Next() + if err == io.EOF { + break + } + if err != nil { + return err + } + + targetPath := filepath.Join(directory, header.Name) + switch header.Typeflag { + case tar.TypeDir: + err := os.MkdirAll(targetPath, 0755) + if err != nil { + return err + } + case tar.TypeReg: + outFile, err := os.Create(targetPath) + if err != nil { + return err + } + defer outFile.Close() + + _, err = io.Copy(outFile, tarReader) + + if err != nil { + return err + } + default: + return errors.New("unsupported file type") + } + + unpackedFiles++ + progressBar.Value = float64(unpackedFiles) / float64(totalFiles) * 100 + fyne.Do(func() { + progressBar.Refresh() + }) + } + + ffmpegPath := filepath.Join(directory, "ffmpeg-master-latest-linux64-gpl", "bin", "ffmpeg") + err = os.Chmod(ffmpegPath, 0755) + if err != nil { + return err + } + + ffprobePath := filepath.Join(directory, "ffmpeg-master-latest-linux64-gpl", "bin", "ffprobe") + err = os.Chmod(ffprobePath, 0755) + if err != nil { + 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/internal/ffmpeg/download/service/download_windows.go b/internal/ffmpeg/download/service/download_windows.go new file mode 100644 index 0000000..f3bc33f --- /dev/null +++ b/internal/ffmpeg/download/service/download_windows.go @@ -0,0 +1,175 @@ +//go:build windows +// +build windows + +package service + +import ( + "archive/zip" + "errors" + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/lang" + "fyne.io/fyne/v2/widget" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application" + "io" + "net/http" + "os" + "path/filepath" + "strings" +) + +func startDownload(app application.AppContract, progressBar *widget.ProgressBar, progressMessage *canvas.Text, save func(ffmpegPath string, ffprobePath string, ffplayPath string) error) error { + var err error + + dir := os.Getenv("APPDATA") + dir = filepath.Join(dir, "fyne", app.FyneApp().UniqueID()) + err = os.MkdirAll(dir, 0755) + if err != nil { + return err + } + + fyne.Do(func() { + progressMessage.Text = lang.L("downloadRun") + progressMessage.Refresh() + }) + err = downloadFile(dir+"/ffmpeg.zip", "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip", progressBar) + if err != nil { + return err + } + + fyne.Do(func() { + progressMessage.Text = lang.L("unzipRun") + progressMessage.Refresh() + }) + err = unZip(dir+"/ffmpeg.zip", dir, progressBar) + if err != nil { + return err + } + _ = os.Remove(dir + "/ffmpeg.zip") + + fyne.Do(func() { + progressMessage.Text = lang.L("testFF") + progressMessage.Refresh() + }) + err = save( + dir+"/ffmpeg-master-latest-win64-gpl/bin/ffmpeg.exe", + dir+"/ffmpeg-master-latest-win64-gpl/bin/ffprobe.exe", + dir+"/ffmpeg-master-latest-win64-gpl/bin/ffplay.exe", + ) + if err != nil { + return err + } + + fyne.Do(func() { + progressMessage.Text = lang.L("completedQueue") + progressMessage.Refresh() + }) + + return nil +} + +func downloadFile(filepath string, url string, progressBar *widget.ProgressBar) (err error) { + progressBar.Value = 0 + progressBar.Max = 100 + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return err + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + f, err := os.OpenFile(filepath, os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return err + } + defer f.Close() + + buf := make([]byte, 32*1024) + var downloaded int64 + for { + n, err := resp.Body.Read(buf) + if err != nil { + if err == io.EOF { + break + } + return err + } + if n > 0 { + f.Write(buf[:n]) + downloaded += int64(n) + progressBar.Value = float64(downloaded) / float64(resp.ContentLength) * 100 + fyne.Do(func() { + progressBar.Refresh() + }) + } + } + return nil +} + +func unZip(fileZip string, directory string, progressBar *widget.ProgressBar) error { + progressBar.Value = 0 + progressBar.Max = 100 + + fyne.Do(func() { + progressBar.Refresh() + }) + + archive, err := zip.OpenReader(fileZip) + if err != nil { + return err + } + defer archive.Close() + + totalBytes := int64(0) + for _, f := range archive.File { + totalBytes += int64(f.UncompressedSize64) + } + + unpackedBytes := int64(0) + + for _, f := range archive.File { + filePath := filepath.Join(directory, f.Name) + + if !strings.HasPrefix(filePath, filepath.Clean(directory)+string(os.PathSeparator)) { + return errors.New("invalid file path") + } + if f.FileInfo().IsDir() { + os.MkdirAll(filePath, os.ModePerm) + continue + } + + if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil { + return err + } + + dstFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + return err + } + + fileInArchive, err := f.Open() + if err != nil { + return err + } + + bytesRead, err := io.Copy(dstFile, fileInArchive) + if err != nil { + return err + } + + unpackedBytes += bytesRead + progressBar.Value = float64(unpackedBytes) / float64(totalBytes) * 100 + fyne.Do(func() { + progressBar.Refresh() + }) + + dstFile.Close() + fileInArchive.Close() + } + + return nil +} diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index c7e3019..ccbb577 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -1,19 +1,52 @@ package ffmpeg +import ( + "errors" + "fyne.io/fyne/v2/lang" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/utils" + "os/exec" + "strings" +) + type FFmpegContract interface { - SetPath(path string) + GetPath() string } type ffmpeg struct { path string } -func newFFmpeg(path string) FFmpegContract { +func newFFmpeg(path string) (FFmpegContract, error) { + if path == "" { + return nil, errors.New(lang.L("errorFFmpeg")) + } + + isCheck, err := checkFFmpegPath(path) + if err != nil { + return nil, err + } + if isCheck == false { + return nil, errors.New(lang.L("errorFFmpeg")) + } + return &ffmpeg{ path: path, - } + }, nil } -func (f *ffmpeg) SetPath(path string) { - f.path = path +func (f *ffmpeg) GetPath() string { + return f.path +} + +func checkFFmpegPath(path string) (bool, error) { + cmd := exec.Command(path, "-version") + utils.PrepareBackgroundCommand(cmd) + out, err := cmd.CombinedOutput() + if err != nil { + return false, err + } + if strings.Contains(strings.TrimSpace(string(out)), "ffmpeg") == false { + return false, nil + } + return true, nil } diff --git a/internal/ffmpeg/ffplay.go b/internal/ffmpeg/ffplay.go index 105359f..6fdf209 100644 --- a/internal/ffmpeg/ffplay.go +++ b/internal/ffmpeg/ffplay.go @@ -1,19 +1,52 @@ package ffmpeg +import ( + "errors" + "fyne.io/fyne/v2/lang" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/utils" + "os/exec" + "strings" +) + type FFplayContract interface { - SetPath(path string) + GetPath() string } type ffplay struct { path string } -func newFFplay(path string) FFplayContract { +func newFFplay(path string) (FFplayContract, error) { + if path == "" { + return nil, errors.New(lang.L("errorFFplay")) + } + + isCheck, err := checkFFplayPath(path) + if err != nil { + return nil, err + } + if isCheck == false { + return nil, errors.New(lang.L("errorFFplay")) + } + return &ffplay{ path: path, - } + }, nil } -func (f *ffplay) SetPath(path string) { - f.path = path +func (f *ffplay) GetPath() string { + return f.path +} + +func checkFFplayPath(path string) (bool, error) { + cmd := exec.Command(path, "-version") + utils.PrepareBackgroundCommand(cmd) + out, err := cmd.CombinedOutput() + if err != nil { + return false, err + } + if strings.Contains(strings.TrimSpace(string(out)), "ffplay") == false { + return false, nil + } + return true, nil } diff --git a/internal/ffmpeg/ffprobe.go b/internal/ffmpeg/ffprobe.go index ee8d9c8..8aa18fc 100644 --- a/internal/ffmpeg/ffprobe.go +++ b/internal/ffmpeg/ffprobe.go @@ -1,19 +1,52 @@ package ffmpeg +import ( + "errors" + "fyne.io/fyne/v2/lang" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/utils" + "os/exec" + "strings" +) + type FFprobeContract interface { - SetPath(path string) + GetPath() string } type ffprobe struct { path string } -func newFFprobe(path string) FFprobeContract { +func newFFprobe(path string) (FFprobeContract, error) { + if path == "" { + return nil, errors.New(lang.L("errorFFprobe")) + } + + isCheck, err := checkFFprobePath(path) + if err != nil { + return nil, err + } + if isCheck == false { + return nil, errors.New(lang.L("errorFFprobe")) + } + return &ffprobe{ path: path, - } + }, nil } -func (f *ffprobe) SetPath(path string) { - f.path = path +func (f *ffprobe) GetPath() string { + return f.path +} + +func checkFFprobePath(path string) (bool, error) { + cmd := exec.Command(path, "-version") + utils.PrepareBackgroundCommand(cmd) + out, err := cmd.CombinedOutput() + if err != nil { + return false, err + } + if strings.Contains(strings.TrimSpace(string(out)), "ffprobe") == false { + return false, nil + } + return true, nil } diff --git a/internal/ffmpeg/utilities.go b/internal/ffmpeg/utilities.go index 309019d..c8c6428 100644 --- a/internal/ffmpeg/utilities.go +++ b/internal/ffmpeg/utilities.go @@ -1,13 +1,25 @@ package ffmpeg import ( + "errors" + "fyne.io/fyne/v2/lang" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application/setting" ) type UtilitiesContract interface { - GetFFmpeg() FFmpegContract - GetFFprobe() FFprobeContract - GetFFplay() FFplayContract + UtilityCheck() bool + + GetFFmpeg() (FFmpegContract, error) + GetFFmpegPath() string + ChangeFFmpeg(path string) error + + GetFFprobe() (FFprobeContract, error) + GetFFprobePath() string + ChangeFFprobe(path string) error + + GetFFplay() (FFplayContract, error) + GetFFplayPath() string + ChangeFFplay(path string) error } type utilities struct { @@ -23,26 +35,131 @@ func NewUtilities(setting setting.SettingContract) UtilitiesContract { } } -func (u *utilities) GetFFmpeg() FFmpegContract { +func (u *utilities) UtilityCheck() bool { + var err error + + _, err = u.GetFFmpeg() + if err != nil { + return false + } + + _, err = u.GetFFprobe() + if err != nil { + return false + } + + _, err = u.GetFFplay() + if err != nil { + return false + } + + return true +} + +func (u *utilities) GetFFmpeg() (FFmpegContract, error) { if u.ffmpeg == nil { - u.ffmpeg = newFFmpeg(u.setting.GetFFmpegPath()) + createFFmpeg, err := newFFmpeg(u.setting.GetFFmpegPath()) + if err != nil { + return nil, err + } + u.ffmpeg = createFFmpeg } - return u.ffmpeg + return u.ffmpeg, nil } -func (u *utilities) GetFFprobe() FFprobeContract { - if u.ffprobe != nil { - u.ffprobe = newFFprobe(u.setting.GetFFprobePath()) +func (u *utilities) GetFFmpegPath() string { + ffmpegService, err := u.GetFFmpeg() + if err != nil { + return "" + } + return ffmpegService.GetPath() +} + +func (u *utilities) ChangeFFmpeg(path string) error { + if path == "" { + return errors.New(lang.L("errorFFmpeg")) } - return u.ffprobe + createFFmpeg, err := newFFmpeg(path) + if err != nil { + return err + } + + u.ffmpeg = createFFmpeg + u.setting.SetFFmpegPath(path) + + return nil } -func (u *utilities) GetFFplay() FFplayContract { +func (u *utilities) GetFFprobe() (FFprobeContract, error) { + if u.ffprobe == nil { + createFFprobe, err := newFFprobe(u.setting.GetFFprobePath()) + if err != nil { + return nil, err + } + u.ffprobe = createFFprobe + } + + return u.ffprobe, nil +} + +func (u *utilities) GetFFprobePath() string { + ffprobeService, err := u.GetFFprobe() + if err != nil { + return "" + } + return ffprobeService.GetPath() +} + +func (u *utilities) ChangeFFprobe(path string) error { + if path == "" { + return errors.New(lang.L("errorFFprobe")) + } + + createFFprobe, err := newFFprobe(path) + if err != nil { + return err + } + + u.ffprobe = createFFprobe + u.setting.SetFFprobePath(path) + + return nil +} + +func (u *utilities) GetFFplay() (FFplayContract, error) { if u.ffplay == nil { - u.ffplay = newFFplay(u.setting.GetFFplayPath()) + createFFplay, err := newFFplay(u.setting.GetFFplayPath()) + if err != nil { + return nil, err + } + u.ffplay = createFFplay } - return u.ffplay + return u.ffplay, nil +} + +func (u *utilities) GetFFplayPath() string { + ffplayService, err := u.GetFFplay() + if err != nil { + return "" + } + return ffplayService.GetPath() +} + +func (u *utilities) ChangeFFplay(path string) error { + if path == "" { + return errors.New(lang.L("errorFFplay")) + } + + createFFplay, err := newFFplay(path) + if err != nil { + return err + } + + u.ffplay = createFFplay + u.setting.SetFFplayPath(path) + + return nil } diff --git a/internal/gui/view/configuring_ffmpeg_utilities.go b/internal/gui/view/configuring_ffmpeg_utilities.go new file mode 100644 index 0000000..11d8d80 --- /dev/null +++ b/internal/gui/view/configuring_ffmpeg_utilities.go @@ -0,0 +1,129 @@ +package view + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/lang" + "fyne.io/fyne/v2/storage" + "fyne.io/fyne/v2/widget" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/gui/window" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/utils" + "image/color" + "net/url" + "path/filepath" +) + +func ConfiguringFFmpegUtilities( + window window.WindowContract, + currentPathFFmpeg string, + currentPathFFprobe string, + currentPathFFplay string, + save func(ffmpegPath string, ffprobePath string, ffplayPath string) error, + cancel func(), + donwloadFFmpeg fyne.CanvasObject, +) fyne.CanvasObject { + errorMessage := canvas.NewText("", color.RGBA{R: 255, G: 0, B: 0, A: 255}) + errorMessage.TextSize = 16 + errorMessage.TextStyle = fyne.TextStyle{Bold: true} + + link := widget.NewHyperlink("https://ffmpeg.org/download.html", &url.URL{ + Scheme: "https", + Host: "ffmpeg.org", + Path: "download.html", + }) + + ffmpegPath, buttonFFmpeg, buttonFFmpegMessage := configuringFFmpegUtilitiesButtonSelectFile(window, currentPathFFmpeg) + ffprobePath, buttonFFprobe, buttonFFprobeMessage := configuringFFmpegUtilitiesButtonSelectFile(window, currentPathFFprobe) + ffplayPath, buttonFFplay, buttonFFplayMessage := configuringFFmpegUtilitiesButtonSelectFile(window, currentPathFFplay) + + form := &widget.Form{ + Items: []*widget.FormItem{ + { + Text: lang.L("titleDownloadLink"), + Widget: link, + }, + { + Text: lang.L("pathToFfmpeg"), + Widget: buttonFFmpeg, + }, + { + Widget: container.NewHScroll(buttonFFmpegMessage), + }, + { + Text: lang.L("pathToFfprobe"), + Widget: buttonFFprobe, + }, + { + Widget: container.NewHScroll(buttonFFprobeMessage), + }, + { + Text: lang.L("pathToFfplay"), + Widget: buttonFFplay, + }, + { + Widget: container.NewHScroll(buttonFFplayMessage), + }, + { + Widget: container.NewHScroll(errorMessage), + }, + }, + SubmitText: lang.L("save"), + OnSubmit: func() { + err := save(*ffmpegPath, *ffprobePath, *ffplayPath) + if err != nil { + errorMessage.Text = err.Error() + } + }, + } + if cancel != nil { + form.OnCancel = cancel + form.CancelText = lang.L("cancel") + } + + selectFFPathTitle := lang.L("selectFFPathTitle") + + return widget.NewCard(selectFFPathTitle, "", container.NewVBox( + form, + donwloadFFmpeg, + )) +} + +func configuringFFmpegUtilitiesButtonSelectFile(window window.WindowContract, path string) (filePath *string, button *widget.Button, buttonMessage *canvas.Text) { + filePath = &path + + buttonMessage = canvas.NewText(path, color.RGBA{R: 49, G: 127, B: 114, A: 255}) + buttonMessage.TextSize = 16 + buttonMessage.TextStyle = fyne.TextStyle{Bold: true} + + buttonTitle := lang.L("choose") + + var locationURI fyne.ListableURI + if len(path) > 0 { + listableURI := storage.NewFileURI(filepath.Dir(path)) + locationURI, _ = storage.ListerForURI(listableURI) + } + + button = widget.NewButton(buttonTitle, func() { + window.NewFileOpen(func(r fyne.URIReadCloser, err error) { + if err != nil { + buttonMessage.Text = err.Error() + utils.SetStringErrorStyle(buttonMessage) + return + } + if r == nil { + return + } + + path = r.URI().Path() + + buttonMessage.Text = r.URI().Path() + utils.SetStringSuccessStyle(buttonMessage) + + listableURI := storage.NewFileURI(filepath.Dir(r.URI().Path())) + locationURI, _ = storage.ListerForURI(listableURI) + }, locationURI) + }) + + return filePath, button, buttonMessage +} diff --git a/internal/gui/window/main.go b/internal/gui/window/main.go index 77f8e51..3dcd187 100644 --- a/internal/gui/window/main.go +++ b/internal/gui/window/main.go @@ -3,15 +3,18 @@ package window import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/lang" "fyne.io/fyne/v2/widget" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/utils" ) -type MainWindowContract interface { +type WindowContract interface { SetContent(content fyne.CanvasObject) Show() InitLayout() + NewFileOpen(callback func(fyne.URIReadCloser, error), location fyne.ListableURI) *dialog.FileDialog } type mainWindow struct { @@ -20,7 +23,7 @@ type mainWindow struct { progressBarService application.ProgressBarContract } -func NewMainWindow(fyneWindow fyne.Window, progressBarService application.ProgressBarContract) MainWindowContract { +func NewMainWindow(fyneWindow fyne.Window, progressBarService application.ProgressBarContract) WindowContract { fyneWindow.Resize(fyne.Size{Width: 1039, Height: 599}) fyneWindow.CenterOnScreen() @@ -40,6 +43,16 @@ func (w *mainWindow) InitLayout() { }) } +func (w *mainWindow) NewFileOpen(callback func(fyne.URIReadCloser, error), location fyne.ListableURI) *dialog.FileDialog { + fileDialog := dialog.NewFileOpen(callback, w.fyneWindow) + utils.FileDialogResize(fileDialog, w.fyneWindow) + fileDialog.Show() + if location != nil { + fileDialog.SetLocation(location) + } + return fileDialog +} + func (w *mainWindow) SetContent(content fyne.CanvasObject) { fyne.Do(func() { if w.layout == nil { diff --git a/internal/utils/dialog.go b/internal/utils/dialog.go new file mode 100644 index 0000000..992a68a --- /dev/null +++ b/internal/utils/dialog.go @@ -0,0 +1,11 @@ +package utils + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/dialog" +) + +func FileDialogResize(fileDialog *dialog.FileDialog, w fyne.Window) { + contentSize := w.Content().Size() + fileDialog.Resize(fyne.Size{Width: contentSize.Width - 50, Height: contentSize.Height - 50}) +} diff --git a/internal/utils/path_separator.go b/internal/utils/path_separator.go new file mode 100644 index 0000000..9fa65cf --- /dev/null +++ b/internal/utils/path_separator.go @@ -0,0 +1,8 @@ +//go:build !windows +// +build !windows + +package utils + +func PathSeparator() string { + return "/" +} diff --git a/internal/utils/path_separator_window.go b/internal/utils/path_separator_window.go new file mode 100644 index 0000000..2acaa53 --- /dev/null +++ b/internal/utils/path_separator_window.go @@ -0,0 +1,8 @@ +//go:build windows +// +build windows + +package utils + +func PathSeparator() string { + return "\\" +} diff --git a/internal/utils/prepare_background_command.go b/internal/utils/prepare_background_command.go new file mode 100644 index 0000000..da5f9b4 --- /dev/null +++ b/internal/utils/prepare_background_command.go @@ -0,0 +1,12 @@ +//go:build !windows +// +build !windows + +package utils + +import ( + "os/exec" +) + +func PrepareBackgroundCommand(cmd *exec.Cmd) { + +} diff --git a/internal/utils/prepare_background_command_windows.go b/internal/utils/prepare_background_command_windows.go new file mode 100644 index 0000000..00af640 --- /dev/null +++ b/internal/utils/prepare_background_command_windows.go @@ -0,0 +1,13 @@ +//go:build windows +// +build windows + +package utils + +import ( + "os/exec" + "syscall" +) + +func PrepareBackgroundCommand(cmd *exec.Cmd) { + cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} +} diff --git a/internal/utils/text.go b/internal/utils/text.go new file mode 100644 index 0000000..20104dd --- /dev/null +++ b/internal/utils/text.go @@ -0,0 +1,21 @@ +package utils + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "image/color" +) + +func SetStringErrorStyle(text *canvas.Text) { + fyne.Do(func() { + text.Color = color.RGBA{R: 255, G: 0, B: 0, A: 255} + text.Refresh() + }) +} + +func SetStringSuccessStyle(text *canvas.Text) { + fyne.Do(func() { + text.Color = color.RGBA{R: 49, G: 127, B: 114, A: 255} + text.Refresh() + }) +} -- 2.47.2 From 57637606c06bf9df20b2b50f65e60491c8244efa Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sat, 7 Jun 2025 21:27:55 +0500 Subject: [PATCH 06/41] Revive the right block of the program Moved the right part of the program from the old version. Slightly reworked the structure. --- internal/application/app.go | 24 +- .../application/convertor/items_to_convert.go | 133 +++++++ internal/application/convertor/queue.go | 148 ++++++++ internal/controller/main.go | 4 +- internal/gui/window/main.go | 106 ++++-- internal/gui/window/queue.go | 357 ++++++++++++++++++ main.go | 5 +- 7 files changed, 740 insertions(+), 37 deletions(-) create mode 100644 internal/application/convertor/items_to_convert.go create mode 100644 internal/application/convertor/queue.go create mode 100644 internal/gui/window/queue.go diff --git a/internal/application/app.go b/internal/application/app.go index 128c088..5499dbd 100644 --- a/internal/application/app.go +++ b/internal/application/app.go @@ -2,6 +2,7 @@ package application import ( "fyne.io/fyne/v2" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application/convertor" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application/setting" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg" ) @@ -11,6 +12,8 @@ type AppContract interface { GetSetting() setting.SettingContract GetProgressBarService() ProgressBarContract GetFFmpegService() ffmpeg.UtilitiesContract + GetItemsToConvert() convertor.ItemsToConvertContract + GetQueueService() convertor.QueueListContract Run() } @@ -19,14 +22,25 @@ type application struct { setting setting.SettingContract progressBarService ProgressBarContract ffmpegService ffmpeg.UtilitiesContract + itemsToConvert convertor.ItemsToConvertContract + queueService convertor.QueueListContract } -func NewApp(fyneApp fyne.App, setting setting.SettingContract, progressBarService ProgressBarContract, ffmpegService ffmpeg.UtilitiesContract) AppContract { +func NewApp( + fyneApp fyne.App, + setting setting.SettingContract, + progressBarService ProgressBarContract, + ffmpegService ffmpeg.UtilitiesContract, + itemsToConvert convertor.ItemsToConvertContract, + queueService convertor.QueueListContract, +) AppContract { return &application{ fyneApp: fyneApp, setting: setting, progressBarService: progressBarService, ffmpegService: ffmpegService, + itemsToConvert: itemsToConvert, + queueService: queueService, } } @@ -46,6 +60,14 @@ func (a *application) GetFFmpegService() ffmpeg.UtilitiesContract { return a.ffmpegService } +func (a *application) GetItemsToConvert() convertor.ItemsToConvertContract { + return a.itemsToConvert +} + +func (a *application) GetQueueService() convertor.QueueListContract { + return a.queueService +} + func (a *application) Run() { a.fyneApp.Run() } diff --git a/internal/application/convertor/items_to_convert.go b/internal/application/convertor/items_to_convert.go new file mode 100644 index 0000000..420b42a --- /dev/null +++ b/internal/application/convertor/items_to_convert.go @@ -0,0 +1,133 @@ +package convertor + +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/internal/ffmpeg" +) + +type ItemsToConvertContract interface { + Add(file *File) + Clear() + GetItems() map[int]ItemToConvertContract + GetItemsContainer() *fyne.Container + AfterAddingQueue() + GetIsAutoRemove() bool + SetIsAutoRemove(isAutoRemove bool) +} + +type itemsToConvert struct { + ffmpeg ffmpeg.UtilitiesContract + nextId int + items map[int]ItemToConvertContract + itemsContainer *fyne.Container + isAutoRemove bool +} + +func NewItemsToConvert(ffmpeg ffmpeg.UtilitiesContract) ItemsToConvertContract { + return &itemsToConvert{ + ffmpeg: ffmpeg, + nextId: 0, + items: map[int]ItemToConvertContract{}, + itemsContainer: container.NewVBox(), + isAutoRemove: true, + } +} + +func (items *itemsToConvert) GetItemsContainer() *fyne.Container { + return items.itemsContainer +} + +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) GetIsAutoRemove() bool { + return items.isAutoRemove +} + +func (items *itemsToConvert) SetIsAutoRemove(isAutoRemove bool) { + items.isAutoRemove = isAutoRemove +} + +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) ItemToConvertContract { + 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/internal/application/convertor/queue.go b/internal/application/convertor/queue.go new file mode 100644 index 0000000..919957a --- /dev/null +++ b/internal/application/convertor/queue.go @@ -0,0 +1,148 @@ +package convertor + +import "errors" + +type Queue struct { + Setting *ConvertSetting + Status StatusContract + Error error +} + +type StatusContract interface { + Name() string + Ordinal() int +} + +const ( + Waiting = iota + InProgress + Completed + Error +) + +type StatusType uint + +var statusTypeStrings = []string{ + "waiting", + "inProgress", + "completed", + "error", +} + +func (status StatusType) Name() string { + return statusTypeStrings[status] +} + +func (status StatusType) Ordinal() int { + return int(status) +} + +type QueueListenerContract interface { + AddQueue(key int, queue *Queue) + ChangeQueue(key int, queue *Queue) + RemoveQueue(key int, status StatusContract) +} + +type QueueListContract interface { + AddListener(queueListener QueueListenerContract) + GetItems() map[int]*Queue + Add(setting *ConvertSetting) + EventChangeQueue(key int, queue *Queue) + Remove(key int) + GetItem(key int) (*Queue, error) + Next() (key int, queue *Queue) +} + +type queueList struct { + currentKey int + items map[int]*Queue + queue map[int]int + queueListener map[int]QueueListenerContract +} + +func NewQueueList() QueueListContract { + return &queueList{ + currentKey: 0, + items: map[int]*Queue{}, + queueListener: map[int]QueueListenerContract{}, + } +} + +func (l *queueList) GetItems() map[int]*Queue { + return l.items +} + +func (l *queueList) Add(setting *ConvertSetting) { + queue := Queue{ + Setting: setting, + Status: StatusType(Waiting), + } + + l.currentKey += 1 + l.items[l.currentKey] = &queue + l.queue[l.currentKey] = l.currentKey + + l.eventAdd(l.currentKey, &queue) +} + +func (l *queueList) EventChangeQueue(key int, queue *Queue) { + l.eventChange(key, queue) +} + +func (l *queueList) Remove(key int) { + if _, ok := l.queue[key]; ok { + delete(l.queue, key) + } + + if _, ok := l.items[key]; ok { + status := l.items[key].Status + l.eventRemove(key, status) + delete(l.items, key) + } +} + +func (l *queueList) GetItem(key int) (*Queue, error) { + if item, ok := l.items[key]; ok { + return item, nil + } + + return nil, errors.New("key not found") +} + +func (l *queueList) AddListener(queueListener QueueListenerContract) { + l.queueListener[len(l.queueListener)] = queueListener +} + +func (l *queueList) Next() (key int, queue *Queue) { + statusWaiting := StatusType(Waiting) + for key, queueId := range l.queue { + if queue, ok := l.items[queueId]; ok { + if queue.Status == statusWaiting { + return queueId, queue + } + } + + if _, ok := l.queue[key]; ok { + delete(l.queue, key) + } + } + return -1, nil +} + +func (l *queueList) eventAdd(key int, queue *Queue) { + for _, listener := range l.queueListener { + listener.AddQueue(key, queue) + } +} + +func (l *queueList) eventChange(key int, queue *Queue) { + for _, listener := range l.queueListener { + listener.ChangeQueue(key, queue) + } +} + +func (l *queueList) eventRemove(key int, status StatusContract) { + for _, listener := range l.queueListener { + listener.RemoveQueue(key, status) + } +} diff --git a/internal/controller/main.go b/internal/controller/main.go index 11cac23..c1c2efd 100644 --- a/internal/controller/main.go +++ b/internal/controller/main.go @@ -18,10 +18,12 @@ type controller struct { func NewController(app application.AppContract) ControllerContract { fyneWindow := app.FyneApp().NewWindow(app.FyneApp().Metadata().Name) + queueLayout := window.NewQueueLayout() + app.GetQueueService().AddListener(queueLayout) return &controller{ app: app, - window: window.NewMainWindow(fyneWindow, app.GetProgressBarService()), + window: window.NewMainWindow(fyneWindow, app.GetProgressBarService(), app.GetItemsToConvert(), queueLayout), } } diff --git a/internal/gui/window/main.go b/internal/gui/window/main.go index 3dcd187..81d3ea0 100644 --- a/internal/gui/window/main.go +++ b/internal/gui/window/main.go @@ -2,11 +2,14 @@ package window import ( "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/lang" + "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application/convertor" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/utils" ) @@ -20,22 +23,26 @@ type WindowContract interface { type mainWindow struct { fyneWindow fyne.Window layout *fyne.Container + itemsToConvert convertor.ItemsToConvertContract progressBarService application.ProgressBarContract + queueLayout QueueLayoutContract } -func NewMainWindow(fyneWindow fyne.Window, progressBarService application.ProgressBarContract) WindowContract { +func NewMainWindow(fyneWindow fyne.Window, progressBarService application.ProgressBarContract, itemsToConvert convertor.ItemsToConvertContract, queueLayout QueueLayoutContract) WindowContract { fyneWindow.Resize(fyne.Size{Width: 1039, Height: 599}) fyneWindow.CenterOnScreen() return &mainWindow{ fyneWindow: fyneWindow, progressBarService: progressBarService, + itemsToConvert: itemsToConvert, + queueLayout: queueLayout, } } func (w *mainWindow) InitLayout() { fyne.Do(func() { - rContainer := newRightContainer(w.progressBarService.GetContainer()) + rContainer := newRightContainer(w.progressBarService.GetContainer(), w.itemsToConvert, w.queueLayout) layout := container.NewAdaptiveGrid(2, widget.NewLabel(""), rContainer.GetCanvasObject()) w.fyneWindow.SetContent(layout) @@ -72,31 +79,20 @@ func (w *mainWindow) Show() { type RightMainContainerContract interface { GetCanvasObject() fyne.CanvasObject GetTabs() *container.AppTabs - GetAddedFilesContainer() *fyne.Container - GetFileQueueContainer() *fyne.Container SelectFileQueueTab() SelectAddedFilesTab() } type rightMainContainer struct { - container fyne.CanvasObject - - tabs *container.AppTabs - - addedFilesContainer *fyne.Container - addedFilesTab *container.TabItem - - fileQueueContainer *fyne.Container - fileQueueTab *container.TabItem + container fyne.CanvasObject + tabs *container.AppTabs + addedFilesTab *container.TabItem + fileQueueTab *container.TabItem } -func newRightContainer(blockProgressbar *fyne.Container) RightMainContainerContract { - - addedFilesContainer := container.NewVBox() - addedFilesTab := container.NewTabItem(lang.L("addedFilesTitle"), addedFilesContainer) - - fileQueueContainer := container.NewVBox() - fileQueueTab := container.NewTabItem(lang.L("fileQueueTitle"), fileQueueContainer) +func newRightContainer(blockProgressbar *fyne.Container, itemsToConvert convertor.ItemsToConvertContract, queueLayout QueueLayoutContract) RightMainContainerContract { + addedFilesTab := container.NewTabItem(lang.L("addedFilesTitle"), addedFilesContainer(itemsToConvert)) + fileQueueTab := container.NewTabItem(lang.L("fileQueueTitle"), fileQueueContainer(queueLayout)) tabs := container.NewAppTabs( addedFilesTab, @@ -115,12 +111,10 @@ func newRightContainer(blockProgressbar *fyne.Container) RightMainContainerContr ) return &rightMainContainer{ - container: rightContainer, - tabs: tabs, - addedFilesContainer: addedFilesContainer, - addedFilesTab: addedFilesTab, - fileQueueContainer: fileQueueContainer, - fileQueueTab: fileQueueTab, + container: rightContainer, + tabs: tabs, + addedFilesTab: addedFilesTab, + fileQueueTab: fileQueueTab, } } @@ -132,14 +126,6 @@ func (r *rightMainContainer) GetTabs() *container.AppTabs { return r.tabs } -func (r *rightMainContainer) GetAddedFilesContainer() *fyne.Container { - return r.addedFilesContainer -} - -func (r *rightMainContainer) GetFileQueueContainer() *fyne.Container { - return r.fileQueueContainer -} - func (r *rightMainContainer) SelectFileQueueTab() { fyne.Do(func() { r.tabs.Select(r.fileQueueTab) @@ -151,3 +137,55 @@ func (r *rightMainContainer) SelectAddedFilesTab() { r.tabs.Select(r.addedFilesTab) }) } + +func addedFilesContainer(itemsToConvert convertor.ItemsToConvertContract) *fyne.Container { + line := canvas.NewLine(theme.Color(theme.ColorNameFocus)) + line.StrokeWidth = 5 + checkboxAutoRemove := widget.NewCheck( + lang.L("autoClearAfterAddingToQueue"), + func(checked bool) { + itemsToConvert.SetIsAutoRemove(checked) + }, + ) + checkboxAutoRemove.SetChecked(itemsToConvert.GetIsAutoRemove()) + + buttonClear := widget.NewButton( + lang.L("clearAll"), + func() { + itemsToConvert.Clear() + }, + ) + buttonClear.Importance = widget.DangerImportance + return container.NewVBox( + container.NewPadded(), + container.NewBorder(nil, nil, nil, buttonClear, container.NewHScroll(checkboxAutoRemove)), + container.NewPadded(), + line, + container.NewPadded(), + itemsToConvert.GetItemsContainer(), + ) +} + +func fileQueueContainer(queueLayout QueueLayoutContract) *fyne.Container { + title := widget.NewLabel(lang.L("queue")) + title.TextStyle.Bold = true + + line := canvas.NewLine(theme.Color(theme.ColorNameFocus)) + line.StrokeWidth = 5 + + queueLayout.GetQueueStatistics().GetWaiting().SetTitle(lang.L("waitingQueue")) + queueLayout.GetQueueStatistics().GetInProgress().SetTitle(lang.L("inProgressQueue")) + queueLayout.GetQueueStatistics().GetCompleted().SetTitle(lang.L("completedQueue")) + queueLayout.GetQueueStatistics().GetError().SetTitle(lang.L("errorQueue")) + queueLayout.GetQueueStatistics().GetTotal().SetTitle(lang.L("total")) + + return container.NewVBox( + container.NewPadded(), + container.NewHBox(title, queueLayout.GetQueueStatistics().GetCompleted().GetCheckbox(), queueLayout.GetQueueStatistics().GetError().GetCheckbox()), + container.NewHBox(queueLayout.GetQueueStatistics().GetInProgress().GetCheckbox(), queueLayout.GetQueueStatistics().GetWaiting().GetCheckbox(), queueLayout.GetQueueStatistics().GetTotal().GetCheckbox()), + container.NewPadded(), + line, + container.NewPadded(), + queueLayout.GetItemsContainer(), + ) +} diff --git a/internal/gui/window/queue.go b/internal/gui/window/queue.go new file mode 100644 index 0000000..31be767 --- /dev/null +++ b/internal/gui/window/queue.go @@ -0,0 +1,357 @@ +package window + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/widget" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application/convertor" + "strconv" + "strings" +) + +type QueueLayoutContract interface { + GetItemsContainer() *fyne.Container + GetQueueStatistics() QueueStatisticsAllContract + + AddQueue(key int, queue *convertor.Queue) + ChangeQueue(key int, queue *convertor.Queue) + RemoveQueue(key int, status convertor.StatusContract) +} + +type queueLayout struct { + itemsContainer *fyne.Container + queueAllStatistics QueueStatisticsAllContract + items map[int]queueLayoutItem +} + +func NewQueueLayout() QueueLayoutContract { + items := map[int]queueLayoutItem{} + + return &queueLayout{ + itemsContainer: container.NewVBox(), + queueAllStatistics: newQueueAllStatistics(&items), + items: items, + } +} + +func (l *queueLayout) GetItemsContainer() *fyne.Container { + return l.itemsContainer +} + +func (l *queueLayout) GetQueueStatistics() QueueStatisticsAllContract { + return l.queueAllStatistics +} + +func (l *queueLayout) AddQueue(key int, queue *convertor.Queue) { + l.addQueueStatistics() +} + +func (l *queueLayout) ChangeQueue(key int, queue *convertor.Queue) { + l.changeQueueStatistics(queue.Status) +} + +func (l *queueLayout) RemoveQueue(key int, status convertor.StatusContract) { + l.removeQueueStatistics(status) +} + +func (l *queueLayout) addQueueStatistics() { + l.GetQueueStatistics().GetWaiting().Add() + l.GetQueueStatistics().GetTotal().Add() +} + +func (l *queueLayout) changeQueueStatistics(status convertor.StatusContract) { + if status == convertor.StatusType(convertor.InProgress) { + l.GetQueueStatistics().GetWaiting().Remove() + l.GetQueueStatistics().GetInProgress().Add() + return + } + + if status == convertor.StatusType(convertor.Completed) { + l.GetQueueStatistics().GetInProgress().Remove() + l.GetQueueStatistics().GetCompleted().Add() + return + } + + if status == convertor.StatusType(convertor.Error) { + l.GetQueueStatistics().GetInProgress().Remove() + l.GetQueueStatistics().GetError().Add() + return + } +} + +func (l *queueLayout) removeQueueStatistics(status convertor.StatusContract) { + l.GetQueueStatistics().GetTotal().Remove() + + if status == convertor.StatusType(convertor.Completed) { + l.GetQueueStatistics().GetCompleted().Remove() + return + } + + if status == convertor.StatusType(convertor.Error) { + l.GetQueueStatistics().GetError().Remove() + return + } + + if status == convertor.StatusType(convertor.InProgress) { + l.GetQueueStatistics().GetInProgress().Remove() + return + } + + if status == convertor.StatusType(convertor.Waiting) { + l.GetQueueStatistics().GetWaiting().Remove() + return + } +} + +type QueueStatisticsAllContract interface { + GetWaiting() QueueStatisticsContract + GetInProgress() QueueStatisticsContract + GetCompleted() QueueStatisticsContract + GetError() QueueStatisticsContract + GetTotal() QueueStatisticsContract +} + +type queueAllStatistics struct { + waiting QueueStatisticsContract + inProgress QueueStatisticsContract + completed QueueStatisticsContract + error QueueStatisticsContract + total QueueStatisticsContract +} + +func newQueueAllStatistics(queueItems *map[int]queueLayoutItem) QueueStatisticsAllContract { + checkboxWaiting := newQueueStatistics() + checkboxInProgress := newQueueStatistics() + checkboxCompleted := newQueueStatistics() + checkboxError := newQueueStatistics() + CheckboxTotal := newQueueStatistics() + + queueAllStatistics := &queueAllStatistics{ + waiting: checkboxWaiting, + inProgress: checkboxInProgress, + completed: checkboxCompleted, + error: checkboxError, + total: CheckboxTotal, + } + + CheckboxTotal.GetCheckbox().OnChanged = func(b bool) { + if b == true { + queueAllStatistics.allCheckboxChecked() + } else { + queueAllStatistics.allUnCheckboxChecked() + } + queueAllStatistics.redrawingQueueItems(queueItems) + } + + checkboxWaiting.GetCheckbox().OnChanged = func(b bool) { + if b == true { + queueAllStatistics.checkboxChecked() + } else { + queueAllStatistics.unCheckboxChecked() + } + queueAllStatistics.redrawingQueueItems(queueItems) + } + + checkboxInProgress.GetCheckbox().OnChanged = func(b bool) { + if b == true { + queueAllStatistics.checkboxChecked() + } else { + queueAllStatistics.unCheckboxChecked() + } + queueAllStatistics.redrawingQueueItems(queueItems) + } + + checkboxCompleted.GetCheckbox().OnChanged = func(b bool) { + if b == true { + queueAllStatistics.checkboxChecked() + } else { + queueAllStatistics.unCheckboxChecked() + } + queueAllStatistics.redrawingQueueItems(queueItems) + } + + checkboxError.GetCheckbox().OnChanged = func(b bool) { + if b == true { + queueAllStatistics.checkboxChecked() + } else { + queueAllStatistics.unCheckboxChecked() + } + queueAllStatistics.redrawingQueueItems(queueItems) + } + + return queueAllStatistics +} + +func (s *queueAllStatistics) GetWaiting() QueueStatisticsContract { + return s.waiting +} + +func (s *queueAllStatistics) GetInProgress() QueueStatisticsContract { + return s.inProgress +} + +func (s *queueAllStatistics) GetCompleted() QueueStatisticsContract { + return s.completed +} + +func (s *queueAllStatistics) GetError() QueueStatisticsContract { + return s.error +} + +func (s *queueAllStatistics) GetTotal() QueueStatisticsContract { + return s.total +} + +func (s *queueAllStatistics) redrawingQueueItems(queueItems *map[int]queueLayoutItem) { + for _, item := range *queueItems { + if s.isChecked(item.status) == true && item.CanvasObject.Visible() == false { + item.CanvasObject.Show() + continue + } + if s.isChecked(item.status) == false && item.CanvasObject.Visible() == true { + item.CanvasObject.Hide() + } + } +} + +func (s *queueAllStatistics) isChecked(status convertor.StatusContract) bool { + if status == convertor.StatusType(convertor.InProgress) { + return s.inProgress.GetCheckbox().Checked + } + if status == convertor.StatusType(convertor.Completed) { + return s.completed.GetCheckbox().Checked + } + if status == convertor.StatusType(convertor.Error) { + return s.error.GetCheckbox().Checked + } + if status == convertor.StatusType(convertor.Waiting) { + return s.waiting.GetCheckbox().Checked + } + + return true +} + +func (s *queueAllStatistics) checkboxChecked() { + if s.total.GetCheckbox().Checked == true { + return + } + + if s.waiting.GetCheckbox().Checked == false { + return + } + + if s.inProgress.GetCheckbox().Checked == false { + return + } + + if s.completed.GetCheckbox().Checked == false { + return + } + + if s.error.GetCheckbox().Checked == false { + return + } + + s.total.GetCheckbox().Checked = true + s.total.GetCheckbox().Refresh() +} + +func (s *queueAllStatistics) unCheckboxChecked() { + if s.total.GetCheckbox().Checked == false { + return + } + + s.total.GetCheckbox().Checked = false + s.total.GetCheckbox().Refresh() +} + +func (s *queueAllStatistics) allCheckboxChecked() { + s.waiting.GetCheckbox().Checked = true + s.waiting.GetCheckbox().Refresh() + + s.inProgress.GetCheckbox().Checked = true + s.inProgress.GetCheckbox().Refresh() + + s.completed.GetCheckbox().Checked = true + s.completed.GetCheckbox().Refresh() + + s.error.GetCheckbox().Checked = true + s.error.GetCheckbox().Refresh() +} + +func (s *queueAllStatistics) allUnCheckboxChecked() { + s.waiting.GetCheckbox().Checked = false + s.waiting.GetCheckbox().Refresh() + + s.inProgress.GetCheckbox().Checked = false + s.inProgress.GetCheckbox().Refresh() + + s.completed.GetCheckbox().Checked = false + s.completed.GetCheckbox().Refresh() + + s.error.GetCheckbox().Checked = false + s.error.GetCheckbox().Refresh() +} + +type QueueStatisticsContract interface { + SetTitle(title string) + GetCheckbox() *widget.Check + Add() + Remove() +} + +type queueStatistics struct { + checkbox *widget.Check + title string + count int64 +} + +func newQueueStatistics() QueueStatisticsContract { + checkbox := widget.NewCheck(": 0", nil) + checkbox.Checked = true + + return &queueStatistics{ + checkbox: checkbox, + title: "", + count: 0, + } +} + +func (s *queueStatistics) SetTitle(title string) { + s.title = strings.ToLower(title) + s.checkbox.Text = title + ": " + strconv.FormatInt(s.count, 10) +} + +func (s *queueStatistics) GetCheckbox() *widget.Check { + return s.checkbox +} + +func (s *queueStatistics) Add() { + s.count += 1 + s.formatText() +} + +func (s *queueStatistics) Remove() { + if s.count == 0 { + return + } + s.count -= 1 + s.formatText() +} + +func (s *queueStatistics) formatText() { + fyne.Do(func() { + s.checkbox.Text = s.title + ": " + strconv.FormatInt(s.count, 10) + s.checkbox.Refresh() + }) +} + +type queueLayoutItem struct { + CanvasObject fyne.CanvasObject + BlockMessageError *container.Scroll + StatusMessage *canvas.Text + MessageError *canvas.Text + buttonPlay *widget.Button + status convertor.StatusContract +} diff --git a/main.go b/main.go index 30a9ae7..95a2e04 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application/convertor" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application/setting" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/controller" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg" @@ -22,7 +23,9 @@ func main() { progressBarService := application.NewProgressBar() appSetting := setting.NewSetting(fyneApp) ffmpegService := ffmpeg.NewUtilities(appSetting) - myApp := application.NewApp(fyneApp, appSetting, progressBarService, ffmpegService) + itemsToConvert := convertor.NewItemsToConvert(ffmpegService) + queue := convertor.NewQueueList() + myApp := application.NewApp(fyneApp, appSetting, progressBarService, ffmpegService, itemsToConvert, queue) mainController := controller.NewController(myApp) mainController.Start() myApp.Run() -- 2.47.2 From 6e8b148c81caf0dbc1bdd6750033d4f42612ed75 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sat, 7 Jun 2025 23:43:32 +0500 Subject: [PATCH 07/41] Made it so that you can play files through FFplay. --- internal/ffmpeg/ffplay.go | 8 ++++++++ internal/ffmpeg/utilities.go | 14 ++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/internal/ffmpeg/ffplay.go b/internal/ffmpeg/ffplay.go index 6fdf209..c888842 100644 --- a/internal/ffmpeg/ffplay.go +++ b/internal/ffmpeg/ffplay.go @@ -10,6 +10,7 @@ import ( type FFplayContract interface { GetPath() string + Play(file *File) error } type ffplay struct { @@ -38,6 +39,13 @@ func (f *ffplay) GetPath() string { return f.path } +func (f *ffplay) Play(file *File) error { + args := []string{file.Path} + cmd := exec.Command(f.GetPath(), args...) + + return cmd.Run() +} + func checkFFplayPath(path string) (bool, error) { cmd := exec.Command(path, "-version") utils.PrepareBackgroundCommand(cmd) diff --git a/internal/ffmpeg/utilities.go b/internal/ffmpeg/utilities.go index c8c6428..5c7c891 100644 --- a/internal/ffmpeg/utilities.go +++ b/internal/ffmpeg/utilities.go @@ -4,8 +4,22 @@ import ( "errors" "fyne.io/fyne/v2/lang" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application/setting" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" ) +type File struct { + Path string + Name string + Ext string +} + +type ConvertSetting struct { + VideoFileInput File + VideoFileOut File + OverwriteOutputFiles bool + Encoder encoder.EncoderContract +} + type UtilitiesContract interface { UtilityCheck() bool -- 2.47.2 From 394824ce88df4c0f36d5f99eea313a3d30650aaa Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sat, 7 Jun 2025 23:44:47 +0500 Subject: [PATCH 08/41] Add layout system and file selection logic Introduce a new layout system for managing main window content and tabs. Integrate file selection and drag-and-drop functionality for adding files to the conversion list, with automatic tab switching to "Added Files". Refactor existing components to support these features. --- .../application/convertor/items_to_convert.go | 24 ++- internal/application/convertor/queue.go | 11 +- internal/controller/convertor.go | 8 +- internal/controller/main.go | 9 +- internal/gui/view/convertor.go | 149 ++++++++++++++- internal/gui/window/layout.go | 173 ++++++++++++++++++ internal/gui/window/main.go | 152 +++------------ 7 files changed, 381 insertions(+), 145 deletions(-) create mode 100644 internal/gui/window/layout.go diff --git a/internal/application/convertor/items_to_convert.go b/internal/application/convertor/items_to_convert.go index 420b42a..32062c9 100644 --- a/internal/application/convertor/items_to_convert.go +++ b/internal/application/convertor/items_to_convert.go @@ -10,7 +10,7 @@ import ( ) type ItemsToConvertContract interface { - Add(file *File) + Add(file *ffmpeg.File) Clear() GetItems() map[int]ItemToConvertContract GetItemsContainer() *fyne.Container @@ -41,7 +41,7 @@ func (items *itemsToConvert) GetItemsContainer() *fyne.Container { return items.itemsContainer } -func (items *itemsToConvert) Add(file *File) { +func (items *itemsToConvert) Add(file *ffmpeg.File) { nextId := items.nextId var content *fyne.Container var buttonPlay *widget.Button @@ -49,9 +49,15 @@ func (items *itemsToConvert) Add(file *File) { buttonPlay = widget.NewButtonWithIcon("", theme.Icon(theme.IconNameMediaPlay), func() { buttonPlay.Disable() go func() { - //_ = items.ffplayService.Run(FFplaySetting{ - // PathToFile: file.Path, - //}) + ffplay, err := items.ffmpeg.GetFFplay() + if err != nil { + fyne.Do(func() { + buttonPlay.Enable() + }) + return + } + + _ = ffplay.Play(file) fyne.Do(func() { buttonPlay.Enable() }) @@ -108,23 +114,23 @@ func (items *itemsToConvert) Clear() { } type ItemToConvertContract interface { - GetFile() *File + GetFile() *ffmpeg.File GetContent() *fyne.Container } type itemToConvert struct { - file *File + file *ffmpeg.File content *fyne.Container } -func newItemToConvert(file *File, content *fyne.Container) ItemToConvertContract { +func newItemToConvert(file *ffmpeg.File, content *fyne.Container) ItemToConvertContract { return &itemToConvert{ file: file, content: content, } } -func (item *itemToConvert) GetFile() *File { +func (item *itemToConvert) GetFile() *ffmpeg.File { return item.file } diff --git a/internal/application/convertor/queue.go b/internal/application/convertor/queue.go index 919957a..d15beb6 100644 --- a/internal/application/convertor/queue.go +++ b/internal/application/convertor/queue.go @@ -1,9 +1,12 @@ package convertor -import "errors" +import ( + "errors" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg" +) type Queue struct { - Setting *ConvertSetting + Setting *ffmpeg.ConvertSetting Status StatusContract Error error } @@ -46,7 +49,7 @@ type QueueListenerContract interface { type QueueListContract interface { AddListener(queueListener QueueListenerContract) GetItems() map[int]*Queue - Add(setting *ConvertSetting) + Add(setting *ffmpeg.ConvertSetting) EventChangeQueue(key int, queue *Queue) Remove(key int) GetItem(key int) (*Queue, error) @@ -72,7 +75,7 @@ func (l *queueList) GetItems() map[int]*Queue { return l.items } -func (l *queueList) Add(setting *ConvertSetting) { +func (l *queueList) Add(setting *ffmpeg.ConvertSetting) { queue := Queue{ Setting: setting, Status: StatusType(Waiting), diff --git a/internal/controller/convertor.go b/internal/controller/convertor.go index eba2f76..4037000 100644 --- a/internal/controller/convertor.go +++ b/internal/controller/convertor.go @@ -1,15 +1,21 @@ package controller import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/download/service" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/gui/view" ) func (c *controller) convertor() { - content := view.Convertor() + content := view.Convertor(c.window, c.addFileForConversion) c.window.SetContent(content) } +func (c *controller) addFileForConversion(file ffmpeg.File) { + c.app.GetItemsToConvert().Add(&file) + c.window.GetLayout().GetRContainer().SelectAddedFilesTab() +} + func (c *controller) settingConvertor(isAllowCancellation bool) { ffmpegPath := c.app.GetFFmpegService().GetFFmpegPath() ffprobePath := c.app.GetFFmpegService().GetFFprobePath() diff --git a/internal/controller/main.go b/internal/controller/main.go index c1c2efd..db86488 100644 --- a/internal/controller/main.go +++ b/internal/controller/main.go @@ -22,8 +22,13 @@ func NewController(app application.AppContract) ControllerContract { app.GetQueueService().AddListener(queueLayout) return &controller{ - app: app, - window: window.NewMainWindow(fyneWindow, app.GetProgressBarService(), app.GetItemsToConvert(), queueLayout), + app: app, + window: window.NewMainWindow( + fyneWindow, + app.GetProgressBarService(), + app.GetItemsToConvert(), + queueLayout, + ), } } diff --git a/internal/gui/view/convertor.go b/internal/gui/view/convertor.go index db11d16..41a3cb8 100644 --- a/internal/gui/view/convertor.go +++ b/internal/gui/view/convertor.go @@ -1,14 +1,24 @@ package view import ( + "errors" "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/lang" + "fyne.io/fyne/v2/storage" "fyne.io/fyne/v2/widget" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/gui/window" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/utils" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" + "image/color" + "os" + "path/filepath" ) -func Convertor() fyne.CanvasObject { - form := newFormConvertor() +func Convertor(window window.WindowContract, addFileForConversion func(file ffmpeg.File)) fyne.CanvasObject { + form := newFormConvertor(window, addFileForConversion) converterVideoFilesTitle := lang.L("converterVideoFilesTitle") return widget.NewCard(converterVideoFilesTitle, "", container.NewVScroll(form.getForm())) @@ -17,17 +27,146 @@ func Convertor() fyne.CanvasObject { type formConvertor struct { form *widget.Form items []*widget.FormItem + + window window.WindowContract + addFileForConversion func(file ffmpeg.File) } -func newFormConvertor() *formConvertor { +func newFormConvertor(window window.WindowContract, addFileForConversion func(file ffmpeg.File)) *formConvertor { f := widget.NewForm() f.SubmitText = lang.L("converterVideoFilesSubmitTitle") - return &formConvertor{ - form: f, + formConvertor := &formConvertor{ + form: f, + window: window, + addFileForConversion: addFileForConversion, } + + fileForConversion := formConvertor.newFileForConversion() + + items := []*widget.FormItem{ + { + Text: lang.L("fileForConversionTitle"), + Widget: fileForConversion.button, + }, + { + Widget: container.NewHScroll(fileForConversion.message), + }, + } + formConvertor.form.Items = items + formConvertor.items = items + + return formConvertor } func (f *formConvertor) getForm() *widget.Form { return f.form } + +type fileForConversion struct { + button *widget.Button + message *canvas.Text + file *kernel.File + + changeCallbacks map[int]func(err error) +} + +func (f *formConvertor) newFileForConversion() *fileForConversion { + message := canvas.NewText("", color.RGBA{R: 255, G: 0, B: 0, A: 255}) + fileForConversion := &fileForConversion{ + message: message, + + changeCallbacks: map[int]func(err error){}, + } + + buttonTitle := lang.L("choose") + "\n" + + lang.L("or") + "\n" + + lang.L("dragAndDropFiles") + + var locationURI fyne.ListableURI + + fileForConversion.button = widget.NewButton(buttonTitle, func() { + f.window.NewFileOpen(func(r fyne.URIReadCloser, err error) { + fyne.Do(func() { + fileForConversion.message.Text = "" + fileForConversion.message.Refresh() + }) + + if err != nil { + fyne.Do(func() { + fileForConversion.message.Text = err.Error() + fileForConversion.message.Refresh() + }) + fileForConversion.eventSelectFile(err) + return + } + if r == nil { + return + } + + f.addFileForConversion(ffmpeg.File{ + Path: r.URI().Path(), + Name: r.URI().Name(), + Ext: r.URI().Extension(), + }) + + fileForConversion.eventSelectFile(nil) + + listableURI := storage.NewFileURI(filepath.Dir(r.URI().Path())) + locationURI, _ = storage.ListerForURI(listableURI) + }, locationURI) + }) + + f.window.SetOnDropped(func(position fyne.Position, uris []fyne.URI) { + if len(uris) == 0 { + return + } + + isError := false + for _, uri := range uris { + info, err := os.Stat(uri.Path()) + if err != nil { + isError = true + continue + } + if info.IsDir() { + isError = true + continue + } + + f.addFileForConversion(ffmpeg.File{ + Path: uri.Path(), + Name: uri.Name(), + Ext: uri.Extension(), + }) + + fileForConversion.eventSelectFile(nil) + + listableURI := storage.NewFileURI(filepath.Dir(uri.Path())) + locationURI, _ = storage.ListerForURI(listableURI) + } + + if isError { + fileForConversion.message.Text = lang.L("errorDragAndDropFile") + utils.SetStringErrorStyle(fileForConversion.message) + fileForConversion.eventSelectFile(errors.New(fileForConversion.message.Text)) + } else { + fyne.Do(func() { + fileForConversion.message.Text = "" + fileForConversion.message.Refresh() + }) + } + }) + + return fileForConversion +} + +func (c *fileForConversion) addChangeCallback(callback func(err error)) { + c.changeCallbacks[len(c.changeCallbacks)] = callback +} + +func (c *fileForConversion) eventSelectFile(err error) { + for _, changeCallback := range c.changeCallbacks { + changeCallback(err) + } +} diff --git a/internal/gui/window/layout.go b/internal/gui/window/layout.go new file mode 100644 index 0000000..b5423cd --- /dev/null +++ b/internal/gui/window/layout.go @@ -0,0 +1,173 @@ +package window + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/lang" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application/convertor" +) + +type LayoutContract interface { + SetContent(content fyne.CanvasObject) fyne.CanvasObject + GetRContainer() RightMainContainerContract +} + +type layout struct { + layoutContainer *fyne.Container + rContainer RightMainContainerContract +} + +func NewLayout(progressBarService application.ProgressBarContract, itemsToConvert convertor.ItemsToConvertContract, queueLayout QueueLayoutContract) LayoutContract { + rContainer := newRightContainer(progressBarService.GetContainer(), itemsToConvert, queueLayout) + layoutContainer := container.NewAdaptiveGrid(2, widget.NewLabel(""), rContainer.GetCanvasObject()) + + return &layout{ + layoutContainer: layoutContainer, + rContainer: rContainer, + } +} + +func (l *layout) SetContent(content fyne.CanvasObject) fyne.CanvasObject { + l.layoutContainer.Objects[0] = content + return l.layoutContainer +} + +func (l *layout) GetRContainer() RightMainContainerContract { + return l.rContainer +} + +type RightMainContainerContract interface { + GetCanvasObject() fyne.CanvasObject + GetTabs() *container.AppTabs + SelectFileQueueTab() + SelectAddedFilesTab() +} + +type rightMainContainer struct { + container fyne.CanvasObject + tabs *container.AppTabs + addedFilesTab *container.TabItem + fileQueueTab *container.TabItem +} + +func newRightContainer(blockProgressbar *fyne.Container, itemsToConvert convertor.ItemsToConvertContract, queueLayout QueueLayoutContract) RightMainContainerContract { + addedFilesTab := container.NewTabItem(lang.L("addedFilesTitle"), addedFilesContainer(itemsToConvert)) + fileQueueTab := container.NewTabItem(lang.L("fileQueueTitle"), fileQueueContainer(queueLayout)) + + tabs := container.NewAppTabs( + addedFilesTab, + fileQueueTab, + ) + + rightContainer := container.NewBorder( + container.NewVBox( + blockProgressbar, + widget.NewSeparator(), + ), + nil, + nil, + nil, + tabs, + ) + + return &rightMainContainer{ + container: rightContainer, + tabs: tabs, + addedFilesTab: addedFilesTab, + fileQueueTab: fileQueueTab, + } +} + +func (r *rightMainContainer) GetCanvasObject() fyne.CanvasObject { + return r.container +} + +func (r *rightMainContainer) GetTabs() *container.AppTabs { + return r.tabs +} + +func (r *rightMainContainer) SelectFileQueueTab() { + fyne.Do(func() { + r.tabs.Select(r.fileQueueTab) + }) +} + +func (r *rightMainContainer) SelectAddedFilesTab() { + fyne.Do(func() { + r.tabs.Select(r.addedFilesTab) + }) +} + +func addedFilesContainer(itemsToConvert convertor.ItemsToConvertContract) *fyne.Container { + line := canvas.NewLine(theme.Color(theme.ColorNameFocus)) + line.StrokeWidth = 5 + checkboxAutoRemove := widget.NewCheck( + lang.L("autoClearAfterAddingToQueue"), + func(checked bool) { + itemsToConvert.SetIsAutoRemove(checked) + }, + ) + checkboxAutoRemove.SetChecked(itemsToConvert.GetIsAutoRemove()) + + buttonClear := widget.NewButton( + lang.L("clearAll"), + func() { + itemsToConvert.Clear() + }, + ) + buttonClear.Importance = widget.DangerImportance + return container.NewBorder( + container.NewVBox( + container.NewPadded(), + container.NewBorder(nil, nil, nil, buttonClear, container.NewHScroll(checkboxAutoRemove)), + container.NewPadded(), + line, + ), nil, nil, nil, + container.NewVScroll( + container.NewBorder( + nil, nil, nil, container.NewPadded(), + container.NewVBox( + container.NewPadded(), + itemsToConvert.GetItemsContainer(), + ), + ), + ), + ) +} + +func fileQueueContainer(queueLayout QueueLayoutContract) *fyne.Container { + title := widget.NewLabel(lang.L("queue")) + title.TextStyle.Bold = true + + line := canvas.NewLine(theme.Color(theme.ColorNameFocus)) + line.StrokeWidth = 5 + + queueLayout.GetQueueStatistics().GetWaiting().SetTitle(lang.L("waitingQueue")) + queueLayout.GetQueueStatistics().GetInProgress().SetTitle(lang.L("inProgressQueue")) + queueLayout.GetQueueStatistics().GetCompleted().SetTitle(lang.L("completedQueue")) + queueLayout.GetQueueStatistics().GetError().SetTitle(lang.L("errorQueue")) + queueLayout.GetQueueStatistics().GetTotal().SetTitle(lang.L("total")) + + return container.NewBorder( + container.NewVBox( + container.NewPadded(), + container.NewHBox(title, queueLayout.GetQueueStatistics().GetCompleted().GetCheckbox(), queueLayout.GetQueueStatistics().GetError().GetCheckbox()), + container.NewHBox(queueLayout.GetQueueStatistics().GetInProgress().GetCheckbox(), queueLayout.GetQueueStatistics().GetWaiting().GetCheckbox(), queueLayout.GetQueueStatistics().GetTotal().GetCheckbox()), + container.NewPadded(), + line, + ), nil, nil, nil, + container.NewVScroll( + container.NewBorder( + nil, nil, nil, container.NewPadded(), + container.NewVBox( + container.NewPadded(), + queueLayout.GetItemsContainer(), + ), + ), + ), + ) +} diff --git a/internal/gui/window/main.go b/internal/gui/window/main.go index 81d3ea0..990c9d0 100644 --- a/internal/gui/window/main.go +++ b/internal/gui/window/main.go @@ -2,12 +2,7 @@ package window import ( "fyne.io/fyne/v2" - "fyne.io/fyne/v2/canvas" - "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/dialog" - "fyne.io/fyne/v2/lang" - "fyne.io/fyne/v2/theme" - "fyne.io/fyne/v2/widget" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application/convertor" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/utils" @@ -18,17 +13,25 @@ type WindowContract interface { Show() InitLayout() NewFileOpen(callback func(fyne.URIReadCloser, error), location fyne.ListableURI) *dialog.FileDialog + NewFolderOpen(callback func(fyne.ListableURI, error), location fyne.ListableURI) *dialog.FileDialog + SetOnDropped(callback func(position fyne.Position, uris []fyne.URI)) + GetLayout() LayoutContract } type mainWindow struct { fyneWindow fyne.Window - layout *fyne.Container + layout LayoutContract itemsToConvert convertor.ItemsToConvertContract progressBarService application.ProgressBarContract queueLayout QueueLayoutContract } -func NewMainWindow(fyneWindow fyne.Window, progressBarService application.ProgressBarContract, itemsToConvert convertor.ItemsToConvertContract, queueLayout QueueLayoutContract) WindowContract { +func NewMainWindow( + fyneWindow fyne.Window, + progressBarService application.ProgressBarContract, + itemsToConvert convertor.ItemsToConvertContract, + queueLayout QueueLayoutContract, +) WindowContract { fyneWindow.Resize(fyne.Size{Width: 1039, Height: 599}) fyneWindow.CenterOnScreen() @@ -42,14 +45,14 @@ func NewMainWindow(fyneWindow fyne.Window, progressBarService application.Progre func (w *mainWindow) InitLayout() { fyne.Do(func() { - rContainer := newRightContainer(w.progressBarService.GetContainer(), w.itemsToConvert, w.queueLayout) - layout := container.NewAdaptiveGrid(2, widget.NewLabel(""), rContainer.GetCanvasObject()) - w.fyneWindow.SetContent(layout) - - w.layout = layout + w.layout = NewLayout(w.progressBarService, w.itemsToConvert, w.queueLayout) }) } +func (w *mainWindow) GetLayout() LayoutContract { + return w.layout +} + func (w *mainWindow) NewFileOpen(callback func(fyne.URIReadCloser, error), location fyne.ListableURI) *dialog.FileDialog { fileDialog := dialog.NewFileOpen(callback, w.fyneWindow) utils.FileDialogResize(fileDialog, w.fyneWindow) @@ -60,6 +63,16 @@ func (w *mainWindow) NewFileOpen(callback func(fyne.URIReadCloser, error), locat return fileDialog } +func (w *mainWindow) NewFolderOpen(callback func(fyne.ListableURI, error), location fyne.ListableURI) *dialog.FileDialog { + fileDialog := dialog.NewFolderOpen(callback, w.fyneWindow) + utils.FileDialogResize(fileDialog, w.fyneWindow) + fileDialog.Show() + if location != nil { + fileDialog.SetLocation(location) + } + return fileDialog +} + func (w *mainWindow) SetContent(content fyne.CanvasObject) { fyne.Do(func() { if w.layout == nil { @@ -67,8 +80,7 @@ func (w *mainWindow) SetContent(content fyne.CanvasObject) { return } - w.layout.Objects[0] = content - w.fyneWindow.SetContent(w.layout) + w.fyneWindow.SetContent(w.layout.SetContent(content)) }) } @@ -76,116 +88,8 @@ func (w *mainWindow) Show() { w.fyneWindow.Show() } -type RightMainContainerContract interface { - GetCanvasObject() fyne.CanvasObject - GetTabs() *container.AppTabs - SelectFileQueueTab() - SelectAddedFilesTab() -} - -type rightMainContainer struct { - container fyne.CanvasObject - tabs *container.AppTabs - addedFilesTab *container.TabItem - fileQueueTab *container.TabItem -} - -func newRightContainer(blockProgressbar *fyne.Container, itemsToConvert convertor.ItemsToConvertContract, queueLayout QueueLayoutContract) RightMainContainerContract { - addedFilesTab := container.NewTabItem(lang.L("addedFilesTitle"), addedFilesContainer(itemsToConvert)) - fileQueueTab := container.NewTabItem(lang.L("fileQueueTitle"), fileQueueContainer(queueLayout)) - - tabs := container.NewAppTabs( - addedFilesTab, - fileQueueTab, - ) - - rightContainer := container.NewBorder( - container.NewVBox( - blockProgressbar, - widget.NewSeparator(), - ), - nil, - nil, - nil, - tabs, - ) - - return &rightMainContainer{ - container: rightContainer, - tabs: tabs, - addedFilesTab: addedFilesTab, - fileQueueTab: fileQueueTab, - } -} - -func (r *rightMainContainer) GetCanvasObject() fyne.CanvasObject { - return r.container -} - -func (r *rightMainContainer) GetTabs() *container.AppTabs { - return r.tabs -} - -func (r *rightMainContainer) SelectFileQueueTab() { +func (w *mainWindow) SetOnDropped(callback func(position fyne.Position, uris []fyne.URI)) { fyne.Do(func() { - r.tabs.Select(r.fileQueueTab) + w.fyneWindow.SetOnDropped(callback) }) } - -func (r *rightMainContainer) SelectAddedFilesTab() { - fyne.Do(func() { - r.tabs.Select(r.addedFilesTab) - }) -} - -func addedFilesContainer(itemsToConvert convertor.ItemsToConvertContract) *fyne.Container { - line := canvas.NewLine(theme.Color(theme.ColorNameFocus)) - line.StrokeWidth = 5 - checkboxAutoRemove := widget.NewCheck( - lang.L("autoClearAfterAddingToQueue"), - func(checked bool) { - itemsToConvert.SetIsAutoRemove(checked) - }, - ) - checkboxAutoRemove.SetChecked(itemsToConvert.GetIsAutoRemove()) - - buttonClear := widget.NewButton( - lang.L("clearAll"), - func() { - itemsToConvert.Clear() - }, - ) - buttonClear.Importance = widget.DangerImportance - return container.NewVBox( - container.NewPadded(), - container.NewBorder(nil, nil, nil, buttonClear, container.NewHScroll(checkboxAutoRemove)), - container.NewPadded(), - line, - container.NewPadded(), - itemsToConvert.GetItemsContainer(), - ) -} - -func fileQueueContainer(queueLayout QueueLayoutContract) *fyne.Container { - title := widget.NewLabel(lang.L("queue")) - title.TextStyle.Bold = true - - line := canvas.NewLine(theme.Color(theme.ColorNameFocus)) - line.StrokeWidth = 5 - - queueLayout.GetQueueStatistics().GetWaiting().SetTitle(lang.L("waitingQueue")) - queueLayout.GetQueueStatistics().GetInProgress().SetTitle(lang.L("inProgressQueue")) - queueLayout.GetQueueStatistics().GetCompleted().SetTitle(lang.L("completedQueue")) - queueLayout.GetQueueStatistics().GetError().SetTitle(lang.L("errorQueue")) - queueLayout.GetQueueStatistics().GetTotal().SetTitle(lang.L("total")) - - return container.NewVBox( - container.NewPadded(), - container.NewHBox(title, queueLayout.GetQueueStatistics().GetCompleted().GetCheckbox(), queueLayout.GetQueueStatistics().GetError().GetCheckbox()), - container.NewHBox(queueLayout.GetQueueStatistics().GetInProgress().GetCheckbox(), queueLayout.GetQueueStatistics().GetWaiting().GetCheckbox(), queueLayout.GetQueueStatistics().GetTotal().GetCheckbox()), - container.NewPadded(), - line, - container.NewPadded(), - queueLayout.GetItemsContainer(), - ) -} -- 2.47.2 From 6c0abac1c5c5f22363364ce02d52aba8b902d296 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sun, 8 Jun 2025 00:18:56 +0500 Subject: [PATCH 09/41] Add directory selection for saving converted files Introduce logic and UI for selecting and persisting a directory to save converted files. Extend existing components with directory selection buttons, message updates, and storage preferences. --- internal/application/setting/setting.go | 14 +++++ internal/controller/convertor.go | 11 +++- internal/gui/view/convertor.go | 83 ++++++++++++++++++++++++- internal/utils/path.go | 19 ++++++ 4 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 internal/utils/path.go diff --git a/internal/application/setting/setting.go b/internal/application/setting/setting.go index d10d324..f969edc 100644 --- a/internal/application/setting/setting.go +++ b/internal/application/setting/setting.go @@ -10,10 +10,16 @@ type SettingContract interface { GetLanguages() []Lang GetCurrentLangOrDefaultLang() (currentLang Lang, isDefault bool) SetLang(language Lang) error + + GetDirectoryForSaving() string + SetDirectoryForSaving(path string) + GetFFmpegPath() string SetFFmpegPath(path string) + GetFFprobePath() string SetFFprobePath(path string) + GetFFplayPath() string SetFFplayPath(path string) } @@ -63,3 +69,11 @@ func (s *setting) SetLang(language Lang) error { s.fyneApp.Preferences().SetString("language", language.Code) return nil } + +func (s *setting) GetDirectoryForSaving() string { + return s.fyneApp.Preferences().String("directoryForSaving") +} + +func (s *setting) SetDirectoryForSaving(path string) { + s.fyneApp.Preferences().SetString("directoryForSaving", path) +} diff --git a/internal/controller/convertor.go b/internal/controller/convertor.go index 4037000..898e3fd 100644 --- a/internal/controller/convertor.go +++ b/internal/controller/convertor.go @@ -7,7 +7,12 @@ import ( ) func (c *controller) convertor() { - content := view.Convertor(c.window, c.addFileForConversion) + content := view.Convertor( + c.window, + c.addFileForConversion, + c.app.GetSetting().GetDirectoryForSaving(), + c.setDirectoryForSaving, + ) c.window.SetContent(content) } @@ -16,6 +21,10 @@ func (c *controller) addFileForConversion(file ffmpeg.File) { c.window.GetLayout().GetRContainer().SelectAddedFilesTab() } +func (c *controller) setDirectoryForSaving(path string) { + c.app.GetSetting().SetDirectoryForSaving(path) +} + func (c *controller) settingConvertor(isAllowCancellation bool) { ffmpegPath := c.app.GetFFmpegService().GetFFmpegPath() ffprobePath := c.app.GetFFmpegService().GetFFprobePath() diff --git a/internal/gui/view/convertor.go b/internal/gui/view/convertor.go index 41a3cb8..8f52592 100644 --- a/internal/gui/view/convertor.go +++ b/internal/gui/view/convertor.go @@ -17,8 +17,18 @@ import ( "path/filepath" ) -func Convertor(window window.WindowContract, addFileForConversion func(file ffmpeg.File)) fyne.CanvasObject { - form := newFormConvertor(window, addFileForConversion) +func Convertor( + window window.WindowContract, + addFileForConversion func(file ffmpeg.File), + directoryForSavingPath string, + directoryForSaving func(path string), +) fyne.CanvasObject { + form := newFormConvertor( + window, + addFileForConversion, + directoryForSavingPath, + directoryForSaving, + ) converterVideoFilesTitle := lang.L("converterVideoFilesTitle") return widget.NewCard(converterVideoFilesTitle, "", container.NewVScroll(form.getForm())) @@ -30,9 +40,15 @@ type formConvertor struct { window window.WindowContract addFileForConversion func(file ffmpeg.File) + directoryForSaving func(path string) } -func newFormConvertor(window window.WindowContract, addFileForConversion func(file ffmpeg.File)) *formConvertor { +func newFormConvertor( + window window.WindowContract, + addFileForConversion func(file ffmpeg.File), + directoryForSavingPath string, + directoryForSaving func(path string), +) *formConvertor { f := widget.NewForm() f.SubmitText = lang.L("converterVideoFilesSubmitTitle") @@ -40,9 +56,11 @@ func newFormConvertor(window window.WindowContract, addFileForConversion func(fi form: f, window: window, addFileForConversion: addFileForConversion, + directoryForSaving: directoryForSaving, } fileForConversion := formConvertor.newFileForConversion() + directoryForSavingButton := formConvertor.newDirectoryForSaving(directoryForSavingPath) items := []*widget.FormItem{ { @@ -52,6 +70,14 @@ func newFormConvertor(window window.WindowContract, addFileForConversion func(fi { Widget: container.NewHScroll(fileForConversion.message), }, + + { + Text: lang.L("buttonForSelectedDirTitle"), + Widget: directoryForSavingButton.button, + }, + { + Widget: container.NewHScroll(directoryForSavingButton.message), + }, } formConvertor.form.Items = items formConvertor.items = items @@ -161,6 +187,57 @@ func (f *formConvertor) newFileForConversion() *fileForConversion { return fileForConversion } +type directoryForSaving struct { + button *widget.Button + message *canvas.Text + path string +} + +func (f *formConvertor) newDirectoryForSaving(directoryForSavingPath string) *directoryForSaving { + directoryForSaving := &directoryForSaving{ + path: "", + } + + directoryForSaving.message = canvas.NewText("", color.RGBA{R: 255, G: 0, B: 0, A: 255}) + directoryForSaving.message.TextSize = 16 + directoryForSaving.message.TextStyle = fyne.TextStyle{Bold: true} + + buttonTitle := lang.L("choose") + + locationURI, err := utils.PathToListableURI(directoryForSavingPath) + if err == nil { + directoryForSaving.path = locationURI.Path() + directoryForSaving.message.Text = locationURI.Path() + utils.SetStringSuccessStyle(directoryForSaving.message) + } + + directoryForSaving.button = widget.NewButton(buttonTitle, func() { + f.window.NewFolderOpen(func(r fyne.ListableURI, err error) { + if err != nil { + directoryForSaving.message.Text = err.Error() + utils.SetStringErrorStyle(directoryForSaving.message) + return + } + if r == nil { + return + } + + directoryForSaving.path = r.Path() + + directoryForSaving.message.Text = r.Path() + utils.SetStringSuccessStyle(directoryForSaving.message) + locationURI, err = storage.ListerForURI(r) + + if err == nil { + f.directoryForSaving(locationURI.Path()) + } + + }, locationURI) + }) + + return directoryForSaving +} + func (c *fileForConversion) addChangeCallback(callback func(err error)) { c.changeCallbacks[len(c.changeCallbacks)] = callback } diff --git a/internal/utils/path.go b/internal/utils/path.go new file mode 100644 index 0000000..4a35d20 --- /dev/null +++ b/internal/utils/path.go @@ -0,0 +1,19 @@ +package utils + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/storage" +) + +func PathToListableURI(path string) (fyne.ListableURI, error) { + if len(path) > 0 { + path = "file://" + path + } + + uri, err := storage.ParseURI(path) + if err != nil { + return nil, err + } + + return storage.ListerForURI(uri) +} -- 2.47.2 From 9cdfa18fc8459bbfd19c39934a2d5030e1591c51 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sun, 8 Jun 2025 17:26:17 +0500 Subject: [PATCH 10/41] Add initial implementations for encoder handling and conversion logic Introduce encoder modules for various codecs and formats (e.g., h264_nvenc, libx264, libmp3lame). Add `Convertor` logic to retrieve supported formats via FFmpeg utilities and manage encoders for audio, video, and image processing. --- internal/application/app.go | 16 +- internal/application/convertor/convertor.go | 54 ++++++ .../application/convertor/encoder/encoder.go | 84 +++++++++ .../application/convertor/encoder/encoders.go | 74 ++++++++ .../{ => convertor}/progressbar.go | 2 +- internal/ffmpeg/encoder/apng/encoder.go | 21 +++ internal/ffmpeg/encoder/bmp/encoder.go | 21 +++ internal/ffmpeg/encoder/encoder.go | 169 ++++++++++++++++++ internal/ffmpeg/encoder/flv/encoder.go | 21 +++ internal/ffmpeg/encoder/gif/encoder.go | 21 +++ internal/ffmpeg/encoder/h264_nvenc/encoder.go | 58 ++++++ internal/ffmpeg/encoder/libmp3lame/encoder.go | 21 +++ internal/ffmpeg/encoder/libshine/encoder.go | 21 +++ internal/ffmpeg/encoder/libtwolame/encoder.go | 21 +++ internal/ffmpeg/encoder/libvpx/encoder.go | 21 +++ internal/ffmpeg/encoder/libvpx_vp9/encoder.go | 21 +++ internal/ffmpeg/encoder/libwebp/encoder.go | 21 +++ .../ffmpeg/encoder/libwebp_anim/encoder.go | 21 +++ internal/ffmpeg/encoder/libx264/encoder.go | 56 ++++++ internal/ffmpeg/encoder/libx265/encoder.go | 56 ++++++ internal/ffmpeg/encoder/libxvid/encoder.go | 21 +++ internal/ffmpeg/encoder/mjpeg/encoder.go | 21 +++ internal/ffmpeg/encoder/mp2/encoder.go | 21 +++ internal/ffmpeg/encoder/mp2fixed/encoder.go | 21 +++ internal/ffmpeg/encoder/mpeg1video/encoder.go | 21 +++ internal/ffmpeg/encoder/mpeg2video/encoder.go | 21 +++ internal/ffmpeg/encoder/mpeg4/encoder.go | 21 +++ internal/ffmpeg/encoder/msmpeg4/encoder.go | 21 +++ internal/ffmpeg/encoder/msmpeg4v2/encoder.go | 21 +++ internal/ffmpeg/encoder/msvideo1/encoder.go | 21 +++ internal/ffmpeg/encoder/png/encoder.go | 21 +++ internal/ffmpeg/encoder/qtrle/encoder.go | 21 +++ internal/ffmpeg/encoder/sgi/encoder.go | 21 +++ internal/ffmpeg/encoder/tiff/encoder.go | 21 +++ internal/ffmpeg/encoder/wmav1/encoder.go | 21 +++ internal/ffmpeg/encoder/wmav2/encoder.go | 21 +++ internal/ffmpeg/encoder/wmv1/encoder.go | 21 +++ internal/ffmpeg/encoder/wmv2/encoder.go | 21 +++ internal/ffmpeg/encoder/xbm/encoder.go | 21 +++ internal/ffmpeg/ffmpeg.go | 22 +++ .../gui/view/convertor/encoders/encoders.go | 15 ++ main.go | 13 +- 42 files changed, 1242 insertions(+), 7 deletions(-) create mode 100644 internal/application/convertor/convertor.go create mode 100644 internal/application/convertor/encoder/encoder.go create mode 100644 internal/application/convertor/encoder/encoders.go rename internal/application/{ => convertor}/progressbar.go (98%) create mode 100644 internal/ffmpeg/encoder/apng/encoder.go create mode 100644 internal/ffmpeg/encoder/bmp/encoder.go create mode 100644 internal/ffmpeg/encoder/encoder.go create mode 100644 internal/ffmpeg/encoder/flv/encoder.go create mode 100644 internal/ffmpeg/encoder/gif/encoder.go create mode 100644 internal/ffmpeg/encoder/h264_nvenc/encoder.go create mode 100644 internal/ffmpeg/encoder/libmp3lame/encoder.go create mode 100644 internal/ffmpeg/encoder/libshine/encoder.go create mode 100644 internal/ffmpeg/encoder/libtwolame/encoder.go create mode 100644 internal/ffmpeg/encoder/libvpx/encoder.go create mode 100644 internal/ffmpeg/encoder/libvpx_vp9/encoder.go create mode 100644 internal/ffmpeg/encoder/libwebp/encoder.go create mode 100644 internal/ffmpeg/encoder/libwebp_anim/encoder.go create mode 100644 internal/ffmpeg/encoder/libx264/encoder.go create mode 100644 internal/ffmpeg/encoder/libx265/encoder.go create mode 100644 internal/ffmpeg/encoder/libxvid/encoder.go create mode 100644 internal/ffmpeg/encoder/mjpeg/encoder.go create mode 100644 internal/ffmpeg/encoder/mp2/encoder.go create mode 100644 internal/ffmpeg/encoder/mp2fixed/encoder.go create mode 100644 internal/ffmpeg/encoder/mpeg1video/encoder.go create mode 100644 internal/ffmpeg/encoder/mpeg2video/encoder.go create mode 100644 internal/ffmpeg/encoder/mpeg4/encoder.go create mode 100644 internal/ffmpeg/encoder/msmpeg4/encoder.go create mode 100644 internal/ffmpeg/encoder/msmpeg4v2/encoder.go create mode 100644 internal/ffmpeg/encoder/msvideo1/encoder.go create mode 100644 internal/ffmpeg/encoder/png/encoder.go create mode 100644 internal/ffmpeg/encoder/qtrle/encoder.go create mode 100644 internal/ffmpeg/encoder/sgi/encoder.go create mode 100644 internal/ffmpeg/encoder/tiff/encoder.go create mode 100644 internal/ffmpeg/encoder/wmav1/encoder.go create mode 100644 internal/ffmpeg/encoder/wmav2/encoder.go create mode 100644 internal/ffmpeg/encoder/wmv1/encoder.go create mode 100644 internal/ffmpeg/encoder/wmv2/encoder.go create mode 100644 internal/ffmpeg/encoder/xbm/encoder.go create mode 100644 internal/gui/view/convertor/encoders/encoders.go diff --git a/internal/application/app.go b/internal/application/app.go index 5499dbd..619a1e4 100644 --- a/internal/application/app.go +++ b/internal/application/app.go @@ -10,8 +10,9 @@ import ( type AppContract interface { FyneApp() fyne.App GetSetting() setting.SettingContract - GetProgressBarService() ProgressBarContract + GetProgressBarService() convertor.ProgressBarContract GetFFmpegService() ffmpeg.UtilitiesContract + GetConvertorService() convertor.ConvertorContract GetItemsToConvert() convertor.ItemsToConvertContract GetQueueService() convertor.QueueListContract Run() @@ -20,19 +21,21 @@ type AppContract interface { type application struct { fyneApp fyne.App setting setting.SettingContract - progressBarService ProgressBarContract + progressBarService convertor.ProgressBarContract ffmpegService ffmpeg.UtilitiesContract itemsToConvert convertor.ItemsToConvertContract queueService convertor.QueueListContract + convertorService convertor.ConvertorContract } func NewApp( fyneApp fyne.App, setting setting.SettingContract, - progressBarService ProgressBarContract, + progressBarService convertor.ProgressBarContract, ffmpegService ffmpeg.UtilitiesContract, itemsToConvert convertor.ItemsToConvertContract, queueService convertor.QueueListContract, + convertorService convertor.ConvertorContract, ) AppContract { return &application{ fyneApp: fyneApp, @@ -41,6 +44,7 @@ func NewApp( ffmpegService: ffmpegService, itemsToConvert: itemsToConvert, queueService: queueService, + convertorService: convertorService, } } @@ -52,7 +56,7 @@ func (a *application) GetSetting() setting.SettingContract { return a.setting } -func (a *application) GetProgressBarService() ProgressBarContract { +func (a *application) GetProgressBarService() convertor.ProgressBarContract { return a.progressBarService } @@ -68,6 +72,10 @@ func (a *application) GetQueueService() convertor.QueueListContract { return a.queueService } +func (a *application) GetConvertorService() convertor.ConvertorContract { + return a.convertorService +} + func (a *application) Run() { a.fyneApp.Run() } diff --git a/internal/application/convertor/convertor.go b/internal/application/convertor/convertor.go new file mode 100644 index 0000000..c36dc56 --- /dev/null +++ b/internal/application/convertor/convertor.go @@ -0,0 +1,54 @@ +package convertor + +import ( + "bufio" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application/convertor/encoder" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg" + "io" + "strings" +) + +type ConvertorContract interface { + GetSupportFormats() (encoder.ConvertorFormatsContract, error) +} + +type convertor struct { + ffmpegService ffmpeg.UtilitiesContract +} + +func NewConvertor( + ffmpegService ffmpeg.UtilitiesContract, +) ConvertorContract { + return &convertor{ + ffmpegService: ffmpegService, + } +} + +func (c *convertor) GetSupportFormats() (encoder.ConvertorFormatsContract, error) { + var err error + + formats := encoder.NewConvertorFormats() + ffmpeg, err := c.ffmpegService.GetFFmpeg() + if err != nil { + return formats, err + } + err = ffmpeg.GetEncoders(func(scanner *bufio.Reader) { + for { + line, _, err := scanner.ReadLine() + if err != nil { + if err == io.EOF { + break + } + continue + } + text := strings.Split(strings.TrimSpace(string(line)), " ") + encoderType := string(text[0][0]) + if len(text) < 2 || (encoderType != "V" && encoderType != "A") { + continue + } + formats.NewEncoder(text[1]) + } + }) + + return formats, err +} diff --git a/internal/application/convertor/encoder/encoder.go b/internal/application/convertor/encoder/encoder.go new file mode 100644 index 0000000..0da7a01 --- /dev/null +++ b/internal/application/convertor/encoder/encoder.go @@ -0,0 +1,84 @@ +package encoder + +import ( + "errors" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" +) + +type ConvertorFormatContract interface { + GetTitle() string + AddEncoder(encoder encoder.EncoderDataContract) + GetFileType() encoder.FileTypeContract + GetEncoders() map[int]encoder.EncoderDataContract +} + +type ConvertorFormat struct { + title string + fileType encoder.FileTypeContract + encoders map[int]encoder.EncoderDataContract +} + +func NewConvertorFormat(title string, fileType encoder.FileTypeContract) ConvertorFormatContract { + return &ConvertorFormat{ + title: title, + fileType: fileType, + encoders: map[int]encoder.EncoderDataContract{}, + } +} + +func (f *ConvertorFormat) GetTitle() string { + return f.title +} + +func (f *ConvertorFormat) AddEncoder(encoder encoder.EncoderDataContract) { + f.encoders[len(f.encoders)] = encoder +} + +func (f *ConvertorFormat) GetEncoders() map[int]encoder.EncoderDataContract { + return f.encoders +} + +func (f *ConvertorFormat) GetFileType() encoder.FileTypeContract { + return f.fileType +} + +type ConvertorFormatsContract interface { + NewEncoder(encoderName string) bool + GetFormats() map[string]ConvertorFormatContract + GetFormat(format string) (ConvertorFormatContract, error) +} + +type ConvertorFormats struct { + formats map[string]ConvertorFormatContract +} + +func NewConvertorFormats() ConvertorFormatsContract { + return &ConvertorFormats{ + formats: map[string]ConvertorFormatContract{}, + } +} + +func (f *ConvertorFormats) NewEncoder(encoderName string) bool { + if supportEncoders[encoderName] == nil { + return false + } + data := supportEncoders[encoderName]() + for _, format := range data.GetFormats() { + if f.formats[format] == nil { + f.formats[format] = NewConvertorFormat(format, data.GetFileType()) + } + f.formats[format].AddEncoder(data) + } + return true +} + +func (f *ConvertorFormats) GetFormats() map[string]ConvertorFormatContract { + return f.formats +} + +func (f *ConvertorFormats) GetFormat(format string) (ConvertorFormatContract, error) { + if f.formats[format] == nil { + return &ConvertorFormat{}, errors.New("not found ConvertorFormat") + } + return f.formats[format], nil +} diff --git a/internal/application/convertor/encoder/encoders.go b/internal/application/convertor/encoder/encoders.go new file mode 100644 index 0000000..b2d2904 --- /dev/null +++ b/internal/application/convertor/encoder/encoders.go @@ -0,0 +1,74 @@ +package encoder + +import ( + encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/apng" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/bmp" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/flv" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/gif" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/h264_nvenc" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/libmp3lame" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/libshine" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/libtwolame" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/libvpx" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/libvpx_vp9" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/libwebp" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/libwebp_anim" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/libx264" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/libx265" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/libxvid" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/mjpeg" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/mp2" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/mp2fixed" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/mpeg1video" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/mpeg2video" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/mpeg4" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/msmpeg4" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/msmpeg4v2" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/msvideo1" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/png" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/qtrle" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/sgi" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/tiff" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/wmav1" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/wmav2" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/wmv1" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/wmv2" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/xbm" +) + +var supportEncoders = map[string]func() encoder2.EncoderDataContract{ + "libx264": libx264.NewData, + "h264_nvenc": h264_nvenc.NewData, + "libx265": libx265.NewData, + "png": png.NewData, + "gif": gif.NewData, + "flv": flv.NewData, + "apng": apng.NewData, + "bmp": bmp.NewData, + "mjpeg": mjpeg.NewData, + "mpeg1video": mpeg1video.NewData, + "mpeg2video": mpeg2video.NewData, + "mpeg4": mpeg4.NewData, + "libxvid": libxvid.NewData, + "msmpeg4v2": msmpeg4v2.NewData, + "msmpeg4": msmpeg4.NewData, + "msvideo1": msvideo1.NewData, + "qtrle": qtrle.NewData, + "tiff": tiff.NewData, + "sgi": sgi.NewData, + "libvpx": libvpx.NewData, + "libvpx-vp9": libvpx_vp9.NewData, + "libwebp_anim": libwebp_anim.NewData, + "libwebp": libwebp.NewData, + "wmv1": wmv1.NewData, + "wmv2": wmv2.NewData, + "xbm": xbm.NewData, + "mp2": mp2.NewData, + "mp2fixed": mp2fixed.NewData, + "libtwolame": libtwolame.NewData, + "libmp3lame": libmp3lame.NewData, + "libshine": libshine.NewData, + "wmav1": wmav1.NewData, + "wmav2": wmav2.NewData, +} diff --git a/internal/application/progressbar.go b/internal/application/convertor/progressbar.go similarity index 98% rename from internal/application/progressbar.go rename to internal/application/convertor/progressbar.go index 8890088..173e994 100644 --- a/internal/application/progressbar.go +++ b/internal/application/convertor/progressbar.go @@ -1,4 +1,4 @@ -package application +package convertor import ( "fyne.io/fyne/v2" diff --git a/internal/ffmpeg/encoder/apng/encoder.go b/internal/ffmpeg/encoder/apng/encoder.go new file mode 100644 index 0000000..8aad4f9 --- /dev/null +++ b/internal/ffmpeg/encoder/apng/encoder.go @@ -0,0 +1,21 @@ +package apng + +import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" +) + +func NewEncoder() encoder.EncoderContract { + parameters := map[string]encoder.ParameterContract{} + getParams := func(parameters map[string]encoder.ParameterContract) []string { + return []string{"-c:v", "apng"} + } + + return encoder.NewEncoder("apng", parameters, getParams) +} + +func NewData() encoder.EncoderDataContract { + title := "apng" + formats := []string{"apng"} + fileType := encoder.FileType(encoder.Image) + return encoder.NewData(title, formats, fileType, NewEncoder) +} diff --git a/internal/ffmpeg/encoder/bmp/encoder.go b/internal/ffmpeg/encoder/bmp/encoder.go new file mode 100644 index 0000000..2d12f86 --- /dev/null +++ b/internal/ffmpeg/encoder/bmp/encoder.go @@ -0,0 +1,21 @@ +package bmp + +import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" +) + +func NewEncoder() encoder.EncoderContract { + parameters := map[string]encoder.ParameterContract{} + getParams := func(parameters map[string]encoder.ParameterContract) []string { + return []string{"-c:v", "bmp"} + } + + return encoder.NewEncoder("bmp", parameters, getParams) +} + +func NewData() encoder.EncoderDataContract { + title := "bmp" + formats := []string{"bmp"} + fileType := encoder.FileType(encoder.Image) + return encoder.NewData(title, formats, fileType, NewEncoder) +} diff --git a/internal/ffmpeg/encoder/encoder.go b/internal/ffmpeg/encoder/encoder.go new file mode 100644 index 0000000..a3eb0ed --- /dev/null +++ b/internal/ffmpeg/encoder/encoder.go @@ -0,0 +1,169 @@ +package encoder + +import "errors" + +type EncoderContract interface { + GetName() string + GetParams() []string + GetParameter(name string) (ParameterContract, error) +} + +type ParameterContract interface { + GetName() string + Set(string) error + Get() string + IsEnabled() bool + SetEnable() + SetDisable() +} + +type EncoderDataContract interface { + GetTitle() string + GetFormats() []string + GetFileType() FileTypeContract + NewEncoder() EncoderContract +} + +type data struct { + title string + formats []string + fileType FileTypeContract + encoder func() EncoderContract +} + +func NewData(title string, formats []string, fileType FileTypeContract, encoder func() EncoderContract) EncoderDataContract { + return &data{ + title: title, + formats: formats, + fileType: fileType, + encoder: encoder, + } +} + +func (data *data) GetTitle() string { + return data.title +} + +func (data *data) GetFormats() []string { + return data.formats +} + +func (data *data) NewEncoder() EncoderContract { + return data.encoder() +} + +func (data *data) GetFileType() FileTypeContract { + return data.fileType +} + +type FileTypeContract interface { + Name() string + Ordinal() int +} + +const ( + Video = iota + Audio + Image +) + +type FileType uint + +var fileTypeStrings = []string{ + "video", + "audio", + "image", +} + +func (fileType FileType) Name() string { + return fileTypeStrings[fileType] +} + +func (fileType FileType) Ordinal() int { + return int(fileType) +} + +func GetListFileType() []FileTypeContract { + return []FileTypeContract{ + FileType(Video), + FileType(Audio), + FileType(Image), + } +} + +type encoder struct { + name string + parameters map[string]ParameterContract + getParams func(parameters map[string]ParameterContract) []string +} + +func NewEncoder(name string, parameters map[string]ParameterContract, getParams func(parameters map[string]ParameterContract) []string) EncoderContract { + return &encoder{ + name: name, + parameters: parameters, + getParams: getParams, + } +} + +func (e *encoder) GetName() string { + return e.name +} + +func (e *encoder) GetParams() []string { + return e.getParams(e.parameters) +} + +func (e *encoder) GetParameter(name string) (ParameterContract, error) { + if e.parameters[name] == nil { + return nil, errors.New("parameter not found") + } + + return e.parameters[name], nil +} + +type parameter struct { + name string + isEnabled bool + parameter string + setParameter func(string) (string, error) +} + +func NewParameter(name string, isEnabled bool, defaultParameter string, setParameter func(string) (string, error)) ParameterContract { + return ¶meter{ + name: name, + isEnabled: isEnabled, + parameter: defaultParameter, + setParameter: setParameter, + } +} + +func (p *parameter) GetName() string { + return p.name +} + +func (p *parameter) Set(s string) (err error) { + if p.setParameter != nil { + s, err = p.setParameter(s) + if err != nil { + return err + } + } + p.parameter = s + return nil +} + +func (p *parameter) Get() string { + return p.parameter +} + +func (p *parameter) IsEnabled() bool { + return p.isEnabled +} + +func (p *parameter) SetEnable() { + p.isEnabled = true +} + +func (p *parameter) SetDisable() { + p.isEnabled = false +} diff --git a/internal/ffmpeg/encoder/flv/encoder.go b/internal/ffmpeg/encoder/flv/encoder.go new file mode 100644 index 0000000..5a2fa14 --- /dev/null +++ b/internal/ffmpeg/encoder/flv/encoder.go @@ -0,0 +1,21 @@ +package flv + +import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" +) + +func NewEncoder() encoder.EncoderContract { + parameters := map[string]encoder.ParameterContract{} + getParams := func(parameters map[string]encoder.ParameterContract) []string { + return []string{"-c:v", "flv"} + } + + return encoder.NewEncoder("flv", parameters, getParams) +} + +func NewData() encoder.EncoderDataContract { + title := "flv" + formats := []string{"flv"} + fileType := encoder.FileType(encoder.Video) + return encoder.NewData(title, formats, fileType, NewEncoder) +} diff --git a/internal/ffmpeg/encoder/gif/encoder.go b/internal/ffmpeg/encoder/gif/encoder.go new file mode 100644 index 0000000..98ae1e9 --- /dev/null +++ b/internal/ffmpeg/encoder/gif/encoder.go @@ -0,0 +1,21 @@ +package gif + +import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" +) + +func NewEncoder() encoder.EncoderContract { + parameters := map[string]encoder.ParameterContract{} + getParams := func(parameters map[string]encoder.ParameterContract) []string { + return []string{"-c:v", "gif"} + } + + return encoder.NewEncoder("gif", parameters, getParams) +} + +func NewData() encoder.EncoderDataContract { + title := "gif" + formats := []string{"gif"} + fileType := encoder.FileType(encoder.Image) + return encoder.NewData(title, formats, fileType, NewEncoder) +} diff --git a/internal/ffmpeg/encoder/h264_nvenc/encoder.go b/internal/ffmpeg/encoder/h264_nvenc/encoder.go new file mode 100644 index 0000000..f52e3f5 --- /dev/null +++ b/internal/ffmpeg/encoder/h264_nvenc/encoder.go @@ -0,0 +1,58 @@ +package h264_nvenc + +import ( + "errors" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" +) + +var Presets = []string{ + "default", + "slow", + "medium", + "fast", + "hp", + "hq", + "bd", + "ll", + "llhq", + "llhp", + "lossless", + "losslesshp", +} + +func NewEncoder() encoder.EncoderContract { + parameters := map[string]encoder.ParameterContract{ + "preset": newParameterPreset(), + } + getParams := func(parameters map[string]encoder.ParameterContract) []string { + params := []string{"-c:v", "h264_nvenc"} + + if parameters["preset"] != nil && parameters["preset"].IsEnabled() { + params = append(params, "-preset", parameters["preset"].Get()) + } + + return params + } + + return encoder.NewEncoder("h264_nvenc", parameters, getParams) +} + +func NewData() encoder.EncoderDataContract { + title := "h264_nvenc" + formats := []string{"mp4"} + fileType := encoder.FileType(encoder.Video) + return encoder.NewData(title, formats, fileType, NewEncoder) +} + +func newParameterPreset() encoder.ParameterContract { + setParameter := func(s string) (string, error) { + for _, value := range Presets { + if value == s { + return value, nil + } + } + + return "", errors.New("preset not found") + } + return encoder.NewParameter("preset", false, "default", setParameter) +} diff --git a/internal/ffmpeg/encoder/libmp3lame/encoder.go b/internal/ffmpeg/encoder/libmp3lame/encoder.go new file mode 100644 index 0000000..86112cb --- /dev/null +++ b/internal/ffmpeg/encoder/libmp3lame/encoder.go @@ -0,0 +1,21 @@ +package libmp3lame + +import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" +) + +func NewEncoder() encoder.EncoderContract { + parameters := map[string]encoder.ParameterContract{} + getParams := func(parameters map[string]encoder.ParameterContract) []string { + return []string{"-c:a", "libmp3lame"} + } + + return encoder.NewEncoder("libmp3lame", parameters, getParams) +} + +func NewData() encoder.EncoderDataContract { + title := "libmp3lame" + formats := []string{"mp3"} + fileType := encoder.FileType(encoder.Audio) + return encoder.NewData(title, formats, fileType, NewEncoder) +} diff --git a/internal/ffmpeg/encoder/libshine/encoder.go b/internal/ffmpeg/encoder/libshine/encoder.go new file mode 100644 index 0000000..87df433 --- /dev/null +++ b/internal/ffmpeg/encoder/libshine/encoder.go @@ -0,0 +1,21 @@ +package libshine + +import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" +) + +func NewEncoder() encoder.EncoderContract { + parameters := map[string]encoder.ParameterContract{} + getParams := func(parameters map[string]encoder.ParameterContract) []string { + return []string{"-c:a", "libshine"} + } + + return encoder.NewEncoder("libshine", parameters, getParams) +} + +func NewData() encoder.EncoderDataContract { + title := "libshine" + formats := []string{"mp3"} + fileType := encoder.FileType(encoder.Audio) + return encoder.NewData(title, formats, fileType, NewEncoder) +} diff --git a/internal/ffmpeg/encoder/libtwolame/encoder.go b/internal/ffmpeg/encoder/libtwolame/encoder.go new file mode 100644 index 0000000..9e6623e --- /dev/null +++ b/internal/ffmpeg/encoder/libtwolame/encoder.go @@ -0,0 +1,21 @@ +package libtwolame + +import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" +) + +func NewEncoder() encoder.EncoderContract { + parameters := map[string]encoder.ParameterContract{} + getParams := func(parameters map[string]encoder.ParameterContract) []string { + return []string{"-c:a", "libtwolame"} + } + + return encoder.NewEncoder("libtwolame", parameters, getParams) +} + +func NewData() encoder.EncoderDataContract { + title := "libtwolame" + formats := []string{"mp2"} + fileType := encoder.FileType(encoder.Audio) + return encoder.NewData(title, formats, fileType, NewEncoder) +} diff --git a/internal/ffmpeg/encoder/libvpx/encoder.go b/internal/ffmpeg/encoder/libvpx/encoder.go new file mode 100644 index 0000000..42bd1f2 --- /dev/null +++ b/internal/ffmpeg/encoder/libvpx/encoder.go @@ -0,0 +1,21 @@ +package libvpx + +import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" +) + +func NewEncoder() encoder.EncoderContract { + parameters := map[string]encoder.ParameterContract{} + getParams := func(parameters map[string]encoder.ParameterContract) []string { + return []string{"-c:v", "libvpx"} + } + + return encoder.NewEncoder("libvpx", parameters, getParams) +} + +func NewData() encoder.EncoderDataContract { + title := "libvpx" + formats := []string{"webm", "mkv"} + fileType := encoder.FileType(encoder.Video) + return encoder.NewData(title, formats, fileType, NewEncoder) +} diff --git a/internal/ffmpeg/encoder/libvpx_vp9/encoder.go b/internal/ffmpeg/encoder/libvpx_vp9/encoder.go new file mode 100644 index 0000000..717eb56 --- /dev/null +++ b/internal/ffmpeg/encoder/libvpx_vp9/encoder.go @@ -0,0 +1,21 @@ +package libvpx_vp9 + +import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" +) + +func NewEncoder() encoder.EncoderContract { + parameters := map[string]encoder.ParameterContract{} + getParams := func(parameters map[string]encoder.ParameterContract) []string { + return []string{"-c:v", "libvpx-vp9"} + } + + return encoder.NewEncoder("libvpx_vp9", parameters, getParams) +} + +func NewData() encoder.EncoderDataContract { + title := "libvpx-vp9" + formats := []string{"webm", "mkv"} + fileType := encoder.FileType(encoder.Video) + return encoder.NewData(title, formats, fileType, NewEncoder) +} diff --git a/internal/ffmpeg/encoder/libwebp/encoder.go b/internal/ffmpeg/encoder/libwebp/encoder.go new file mode 100644 index 0000000..5418bbc --- /dev/null +++ b/internal/ffmpeg/encoder/libwebp/encoder.go @@ -0,0 +1,21 @@ +package libwebp + +import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" +) + +func NewEncoder() encoder.EncoderContract { + parameters := map[string]encoder.ParameterContract{} + getParams := func(parameters map[string]encoder.ParameterContract) []string { + return []string{"-c:v", "libwebp"} + } + + return encoder.NewEncoder("libwebp", parameters, getParams) +} + +func NewData() encoder.EncoderDataContract { + title := "libwebp" + formats := []string{"webp"} + fileType := encoder.FileType(encoder.Image) + return encoder.NewData(title, formats, fileType, NewEncoder) +} diff --git a/internal/ffmpeg/encoder/libwebp_anim/encoder.go b/internal/ffmpeg/encoder/libwebp_anim/encoder.go new file mode 100644 index 0000000..549e589 --- /dev/null +++ b/internal/ffmpeg/encoder/libwebp_anim/encoder.go @@ -0,0 +1,21 @@ +package libwebp_anim + +import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" +) + +func NewEncoder() encoder.EncoderContract { + parameters := map[string]encoder.ParameterContract{} + getParams := func(parameters map[string]encoder.ParameterContract) []string { + return []string{"-c:v", "libwebp_anim"} + } + + return encoder.NewEncoder("libwebp_anim", parameters, getParams) +} + +func NewData() encoder.EncoderDataContract { + title := "libwebp_anim" + formats := []string{"webp"} + fileType := encoder.FileType(encoder.Image) + return encoder.NewData(title, formats, fileType, NewEncoder) +} diff --git a/internal/ffmpeg/encoder/libx264/encoder.go b/internal/ffmpeg/encoder/libx264/encoder.go new file mode 100644 index 0000000..0a3070f --- /dev/null +++ b/internal/ffmpeg/encoder/libx264/encoder.go @@ -0,0 +1,56 @@ +package libx264 + +import ( + "errors" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" +) + +var Presets = []string{ + "ultrafast", + "superfast", + "veryfast", + "faster", + "fast", + "medium", + "slow", + "slower", + "veryslow", + "placebo", +} + +func NewEncoder() encoder.EncoderContract { + parameters := map[string]encoder.ParameterContract{ + "preset": newParameterPreset(), + } + getParams := func(parameters map[string]encoder.ParameterContract) []string { + params := []string{"-c:v", "libx264"} + + if parameters["preset"] != nil && parameters["preset"].IsEnabled() { + params = append(params, "-preset", parameters["preset"].Get()) + } + + return params + } + + return encoder.NewEncoder("libx264", parameters, getParams) +} + +func NewData() encoder.EncoderDataContract { + title := "libx264" + formats := []string{"mp4"} + fileType := encoder.FileType(encoder.Video) + return encoder.NewData(title, formats, fileType, NewEncoder) +} + +func newParameterPreset() encoder.ParameterContract { + setParameter := func(s string) (string, error) { + for _, value := range Presets { + if value == s { + return value, nil + } + } + + return "", errors.New("preset not found") + } + return encoder.NewParameter("preset", false, "medium", setParameter) +} diff --git a/internal/ffmpeg/encoder/libx265/encoder.go b/internal/ffmpeg/encoder/libx265/encoder.go new file mode 100644 index 0000000..0d4dbc3 --- /dev/null +++ b/internal/ffmpeg/encoder/libx265/encoder.go @@ -0,0 +1,56 @@ +package libx265 + +import ( + "errors" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" +) + +var Presets = []string{ + "ultrafast", + "superfast", + "veryfast", + "faster", + "fast", + "medium", + "slow", + "slower", + "veryslow", + "placebo", +} + +func NewEncoder() encoder.EncoderContract { + parameters := map[string]encoder.ParameterContract{ + "preset": newParameterPreset(), + } + getParams := func(parameters map[string]encoder.ParameterContract) []string { + params := []string{"-c:v", "libx265"} + + if parameters["preset"] != nil && parameters["preset"].IsEnabled() { + params = append(params, "-preset", parameters["preset"].Get()) + } + + return params + } + + return encoder.NewEncoder("libx265", parameters, getParams) +} + +func NewData() encoder.EncoderDataContract { + title := "libx265" + formats := []string{"mp4"} + fileType := encoder.FileType(encoder.Video) + return encoder.NewData(title, formats, fileType, NewEncoder) +} + +func newParameterPreset() encoder.ParameterContract { + setParameter := func(s string) (string, error) { + for _, value := range Presets { + if value == s { + return value, nil + } + } + + return "", errors.New("preset not found") + } + return encoder.NewParameter("preset", false, "medium", setParameter) +} diff --git a/internal/ffmpeg/encoder/libxvid/encoder.go b/internal/ffmpeg/encoder/libxvid/encoder.go new file mode 100644 index 0000000..6f09a1e --- /dev/null +++ b/internal/ffmpeg/encoder/libxvid/encoder.go @@ -0,0 +1,21 @@ +package libxvid + +import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" +) + +func NewEncoder() encoder.EncoderContract { + parameters := map[string]encoder.ParameterContract{} + getParams := func(parameters map[string]encoder.ParameterContract) []string { + return []string{"-c:v", "libxvid"} + } + + return encoder.NewEncoder("libxvid", parameters, getParams) +} + +func NewData() encoder.EncoderDataContract { + title := "libxvid" + formats := []string{"avi"} + fileType := encoder.FileType(encoder.Video) + return encoder.NewData(title, formats, fileType, NewEncoder) +} diff --git a/internal/ffmpeg/encoder/mjpeg/encoder.go b/internal/ffmpeg/encoder/mjpeg/encoder.go new file mode 100644 index 0000000..3252837 --- /dev/null +++ b/internal/ffmpeg/encoder/mjpeg/encoder.go @@ -0,0 +1,21 @@ +package mjpeg + +import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" +) + +func NewEncoder() encoder.EncoderContract { + parameters := map[string]encoder.ParameterContract{} + getParams := func(parameters map[string]encoder.ParameterContract) []string { + return []string{"-c:v", "mjpeg"} + } + + return encoder.NewEncoder("mjpeg", parameters, getParams) +} + +func NewData() encoder.EncoderDataContract { + title := "mjpeg" + formats := []string{"jpg"} + fileType := encoder.FileType(encoder.Image) + return encoder.NewData(title, formats, fileType, NewEncoder) +} diff --git a/internal/ffmpeg/encoder/mp2/encoder.go b/internal/ffmpeg/encoder/mp2/encoder.go new file mode 100644 index 0000000..da4221d --- /dev/null +++ b/internal/ffmpeg/encoder/mp2/encoder.go @@ -0,0 +1,21 @@ +package mp2 + +import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" +) + +func NewEncoder() encoder.EncoderContract { + parameters := map[string]encoder.ParameterContract{} + getParams := func(parameters map[string]encoder.ParameterContract) []string { + return []string{"-c:a", "mp2"} + } + + return encoder.NewEncoder("mp2", parameters, getParams) +} + +func NewData() encoder.EncoderDataContract { + title := "mp2" + formats := []string{"mp2"} + fileType := encoder.FileType(encoder.Audio) + return encoder.NewData(title, formats, fileType, NewEncoder) +} diff --git a/internal/ffmpeg/encoder/mp2fixed/encoder.go b/internal/ffmpeg/encoder/mp2fixed/encoder.go new file mode 100644 index 0000000..79cb7c7 --- /dev/null +++ b/internal/ffmpeg/encoder/mp2fixed/encoder.go @@ -0,0 +1,21 @@ +package mp2fixed + +import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" +) + +func NewEncoder() encoder.EncoderContract { + parameters := map[string]encoder.ParameterContract{} + getParams := func(parameters map[string]encoder.ParameterContract) []string { + return []string{"-c:a", "mp2fixed"} + } + + return encoder.NewEncoder("mp2fixed", parameters, getParams) +} + +func NewData() encoder.EncoderDataContract { + title := "mp2fixed" + formats := []string{"mp2"} + fileType := encoder.FileType(encoder.Audio) + return encoder.NewData(title, formats, fileType, NewEncoder) +} diff --git a/internal/ffmpeg/encoder/mpeg1video/encoder.go b/internal/ffmpeg/encoder/mpeg1video/encoder.go new file mode 100644 index 0000000..18e7753 --- /dev/null +++ b/internal/ffmpeg/encoder/mpeg1video/encoder.go @@ -0,0 +1,21 @@ +package mpeg1video + +import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" +) + +func NewEncoder() encoder.EncoderContract { + parameters := map[string]encoder.ParameterContract{} + getParams := func(parameters map[string]encoder.ParameterContract) []string { + return []string{"-c:v", "mpeg1video"} + } + + return encoder.NewEncoder("mpeg1video", parameters, getParams) +} + +func NewData() encoder.EncoderDataContract { + title := "mpeg1video" + formats := []string{"mpg", "mpeg"} + fileType := encoder.FileType(encoder.Video) + return encoder.NewData(title, formats, fileType, NewEncoder) +} diff --git a/internal/ffmpeg/encoder/mpeg2video/encoder.go b/internal/ffmpeg/encoder/mpeg2video/encoder.go new file mode 100644 index 0000000..a27bb0c --- /dev/null +++ b/internal/ffmpeg/encoder/mpeg2video/encoder.go @@ -0,0 +1,21 @@ +package mpeg2video + +import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" +) + +func NewEncoder() encoder.EncoderContract { + parameters := map[string]encoder.ParameterContract{} + getParams := func(parameters map[string]encoder.ParameterContract) []string { + return []string{"-c:v", "mpeg2video"} + } + + return encoder.NewEncoder("mpeg2video", parameters, getParams) +} + +func NewData() encoder.EncoderDataContract { + title := "mpeg2video" + formats := []string{"mpg", "mpeg"} + fileType := encoder.FileType(encoder.Video) + return encoder.NewData(title, formats, fileType, NewEncoder) +} diff --git a/internal/ffmpeg/encoder/mpeg4/encoder.go b/internal/ffmpeg/encoder/mpeg4/encoder.go new file mode 100644 index 0000000..6f22876 --- /dev/null +++ b/internal/ffmpeg/encoder/mpeg4/encoder.go @@ -0,0 +1,21 @@ +package mpeg4 + +import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" +) + +func NewEncoder() encoder.EncoderContract { + parameters := map[string]encoder.ParameterContract{} + getParams := func(parameters map[string]encoder.ParameterContract) []string { + return []string{"-c:v", "mpeg4"} + } + + return encoder.NewEncoder("mpeg4", parameters, getParams) +} + +func NewData() encoder.EncoderDataContract { + title := "mpeg4" + formats := []string{"avi"} + fileType := encoder.FileType(encoder.Video) + return encoder.NewData(title, formats, fileType, NewEncoder) +} diff --git a/internal/ffmpeg/encoder/msmpeg4/encoder.go b/internal/ffmpeg/encoder/msmpeg4/encoder.go new file mode 100644 index 0000000..3bcf5d8 --- /dev/null +++ b/internal/ffmpeg/encoder/msmpeg4/encoder.go @@ -0,0 +1,21 @@ +package msmpeg4 + +import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" +) + +func NewEncoder() encoder.EncoderContract { + parameters := map[string]encoder.ParameterContract{} + getParams := func(parameters map[string]encoder.ParameterContract) []string { + return []string{"-c:v", "msmpeg4"} + } + + return encoder.NewEncoder("msmpeg4", parameters, getParams) +} + +func NewData() encoder.EncoderDataContract { + title := "msmpeg4" + formats := []string{"avi"} + fileType := encoder.FileType(encoder.Video) + return encoder.NewData(title, formats, fileType, NewEncoder) +} diff --git a/internal/ffmpeg/encoder/msmpeg4v2/encoder.go b/internal/ffmpeg/encoder/msmpeg4v2/encoder.go new file mode 100644 index 0000000..296730f --- /dev/null +++ b/internal/ffmpeg/encoder/msmpeg4v2/encoder.go @@ -0,0 +1,21 @@ +package msmpeg4v2 + +import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" +) + +func NewEncoder() encoder.EncoderContract { + parameters := map[string]encoder.ParameterContract{} + getParams := func(parameters map[string]encoder.ParameterContract) []string { + return []string{"-c:v", "msmpeg4v2"} + } + + return encoder.NewEncoder("msmpeg4v2", parameters, getParams) +} + +func NewData() encoder.EncoderDataContract { + title := "msmpeg4v2" + formats := []string{"avi"} + fileType := encoder.FileType(encoder.Video) + return encoder.NewData(title, formats, fileType, NewEncoder) +} diff --git a/internal/ffmpeg/encoder/msvideo1/encoder.go b/internal/ffmpeg/encoder/msvideo1/encoder.go new file mode 100644 index 0000000..d9e22ca --- /dev/null +++ b/internal/ffmpeg/encoder/msvideo1/encoder.go @@ -0,0 +1,21 @@ +package msvideo1 + +import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" +) + +func NewEncoder() encoder.EncoderContract { + parameters := map[string]encoder.ParameterContract{} + getParams := func(parameters map[string]encoder.ParameterContract) []string { + return []string{"-c:v", "msvideo1"} + } + + return encoder.NewEncoder("msvideo1", parameters, getParams) +} + +func NewData() encoder.EncoderDataContract { + title := "msvideo1" + formats := []string{"avi"} + fileType := encoder.FileType(encoder.Video) + return encoder.NewData(title, formats, fileType, NewEncoder) +} diff --git a/internal/ffmpeg/encoder/png/encoder.go b/internal/ffmpeg/encoder/png/encoder.go new file mode 100644 index 0000000..06bac92 --- /dev/null +++ b/internal/ffmpeg/encoder/png/encoder.go @@ -0,0 +1,21 @@ +package png + +import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" +) + +func NewEncoder() encoder.EncoderContract { + parameters := map[string]encoder.ParameterContract{} + getParams := func(parameters map[string]encoder.ParameterContract) []string { + return []string{"-c:v", "png"} + } + + return encoder.NewEncoder("png", parameters, getParams) +} + +func NewData() encoder.EncoderDataContract { + title := "png" + formats := []string{"png"} + fileType := encoder.FileType(encoder.Image) + return encoder.NewData(title, formats, fileType, NewEncoder) +} diff --git a/internal/ffmpeg/encoder/qtrle/encoder.go b/internal/ffmpeg/encoder/qtrle/encoder.go new file mode 100644 index 0000000..0641480 --- /dev/null +++ b/internal/ffmpeg/encoder/qtrle/encoder.go @@ -0,0 +1,21 @@ +package qtrle + +import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" +) + +func NewEncoder() encoder.EncoderContract { + parameters := map[string]encoder.ParameterContract{} + getParams := func(parameters map[string]encoder.ParameterContract) []string { + return []string{"-c:v", "qtrle"} + } + + return encoder.NewEncoder("qtrle", parameters, getParams) +} + +func NewData() encoder.EncoderDataContract { + title := "qtrle" + formats := []string{"mov"} + fileType := encoder.FileType(encoder.Video) + return encoder.NewData(title, formats, fileType, NewEncoder) +} diff --git a/internal/ffmpeg/encoder/sgi/encoder.go b/internal/ffmpeg/encoder/sgi/encoder.go new file mode 100644 index 0000000..3d49a3c --- /dev/null +++ b/internal/ffmpeg/encoder/sgi/encoder.go @@ -0,0 +1,21 @@ +package sgi + +import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" +) + +func NewEncoder() encoder.EncoderContract { + parameters := map[string]encoder.ParameterContract{} + getParams := func(parameters map[string]encoder.ParameterContract) []string { + return []string{"-c:v", "sgi"} + } + + return encoder.NewEncoder("sgi", parameters, getParams) +} + +func NewData() encoder.EncoderDataContract { + title := "sgi" + formats := []string{"sgi"} + fileType := encoder.FileType(encoder.Image) + return encoder.NewData(title, formats, fileType, NewEncoder) +} diff --git a/internal/ffmpeg/encoder/tiff/encoder.go b/internal/ffmpeg/encoder/tiff/encoder.go new file mode 100644 index 0000000..ad43a9d --- /dev/null +++ b/internal/ffmpeg/encoder/tiff/encoder.go @@ -0,0 +1,21 @@ +package tiff + +import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" +) + +func NewEncoder() encoder.EncoderContract { + parameters := map[string]encoder.ParameterContract{} + getParams := func(parameters map[string]encoder.ParameterContract) []string { + return []string{"-c:v", "tiff"} + } + + return encoder.NewEncoder("tiff", parameters, getParams) +} + +func NewData() encoder.EncoderDataContract { + title := "tiff" + formats := []string{"tiff"} + fileType := encoder.FileType(encoder.Image) + return encoder.NewData(title, formats, fileType, NewEncoder) +} diff --git a/internal/ffmpeg/encoder/wmav1/encoder.go b/internal/ffmpeg/encoder/wmav1/encoder.go new file mode 100644 index 0000000..2aa5af1 --- /dev/null +++ b/internal/ffmpeg/encoder/wmav1/encoder.go @@ -0,0 +1,21 @@ +package wmav1 + +import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" +) + +func NewEncoder() encoder.EncoderContract { + parameters := map[string]encoder.ParameterContract{} + getParams := func(parameters map[string]encoder.ParameterContract) []string { + return []string{"-c:a", "wmav1"} + } + + return encoder.NewEncoder("wmav1", parameters, getParams) +} + +func NewData() encoder.EncoderDataContract { + title := "wmav1" + formats := []string{"wma"} + fileType := encoder.FileType(encoder.Audio) + return encoder.NewData(title, formats, fileType, NewEncoder) +} diff --git a/internal/ffmpeg/encoder/wmav2/encoder.go b/internal/ffmpeg/encoder/wmav2/encoder.go new file mode 100644 index 0000000..61e2cb5 --- /dev/null +++ b/internal/ffmpeg/encoder/wmav2/encoder.go @@ -0,0 +1,21 @@ +package wmav2 + +import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" +) + +func NewEncoder() encoder.EncoderContract { + parameters := map[string]encoder.ParameterContract{} + getParams := func(parameters map[string]encoder.ParameterContract) []string { + return []string{"-c:a", "wmav2"} + } + + return encoder.NewEncoder("wmav2", parameters, getParams) +} + +func NewData() encoder.EncoderDataContract { + title := "wmav2" + formats := []string{"wma"} + fileType := encoder.FileType(encoder.Audio) + return encoder.NewData(title, formats, fileType, NewEncoder) +} diff --git a/internal/ffmpeg/encoder/wmv1/encoder.go b/internal/ffmpeg/encoder/wmv1/encoder.go new file mode 100644 index 0000000..cab9f49 --- /dev/null +++ b/internal/ffmpeg/encoder/wmv1/encoder.go @@ -0,0 +1,21 @@ +package wmv1 + +import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" +) + +func NewEncoder() encoder.EncoderContract { + parameters := map[string]encoder.ParameterContract{} + getParams := func(parameters map[string]encoder.ParameterContract) []string { + return []string{"-c:v", "wmv1"} + } + + return encoder.NewEncoder("wmv1", parameters, getParams) +} + +func NewData() encoder.EncoderDataContract { + title := "wmv1" + formats := []string{"wmv"} + fileType := encoder.FileType(encoder.Video) + return encoder.NewData(title, formats, fileType, NewEncoder) +} diff --git a/internal/ffmpeg/encoder/wmv2/encoder.go b/internal/ffmpeg/encoder/wmv2/encoder.go new file mode 100644 index 0000000..092ea88 --- /dev/null +++ b/internal/ffmpeg/encoder/wmv2/encoder.go @@ -0,0 +1,21 @@ +package wmv2 + +import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" +) + +func NewEncoder() encoder.EncoderContract { + parameters := map[string]encoder.ParameterContract{} + getParams := func(parameters map[string]encoder.ParameterContract) []string { + return []string{"-c:v", "wmv2"} + } + + return encoder.NewEncoder("wmv2", parameters, getParams) +} + +func NewData() encoder.EncoderDataContract { + title := "wmv2" + formats := []string{"wmv"} + fileType := encoder.FileType(encoder.Video) + return encoder.NewData(title, formats, fileType, NewEncoder) +} diff --git a/internal/ffmpeg/encoder/xbm/encoder.go b/internal/ffmpeg/encoder/xbm/encoder.go new file mode 100644 index 0000000..bc2e612 --- /dev/null +++ b/internal/ffmpeg/encoder/xbm/encoder.go @@ -0,0 +1,21 @@ +package xbm + +import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" +) + +func NewEncoder() encoder.EncoderContract { + parameters := map[string]encoder.ParameterContract{} + getParams := func(parameters map[string]encoder.ParameterContract) []string { + return []string{"-c:v", "xbm"} + } + + return encoder.NewEncoder("xbm", parameters, getParams) +} + +func NewData() encoder.EncoderDataContract { + title := "xbm" + formats := []string{"xbm"} + fileType := encoder.FileType(encoder.Image) + return encoder.NewData(title, formats, fileType, NewEncoder) +} diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index ccbb577..db5a30d 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -1,6 +1,7 @@ package ffmpeg import ( + "bufio" "errors" "fyne.io/fyne/v2/lang" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/utils" @@ -10,6 +11,7 @@ import ( type FFmpegContract interface { GetPath() string + GetEncoders(scanner func(scanner *bufio.Reader)) error } type ffmpeg struct { @@ -38,6 +40,26 @@ func (f *ffmpeg) GetPath() string { return f.path } +func (f *ffmpeg) GetEncoders(scanner func(scanner *bufio.Reader)) error { + cmd := exec.Command(f.path, "-encoders") + utils.PrepareBackgroundCommand(cmd) + + stdOut, err := cmd.StdoutPipe() + if err != nil { + return err + } + + err = cmd.Start() + if err != nil { + return err + } + + scannerErr := bufio.NewReader(stdOut) + scanner(scannerErr) + + return cmd.Wait() +} + func checkFFmpegPath(path string) (bool, error) { cmd := exec.Command(path, "-version") utils.PrepareBackgroundCommand(cmd) diff --git a/internal/gui/view/convertor/encoders/encoders.go b/internal/gui/view/convertor/encoders/encoders.go new file mode 100644 index 0000000..ddbf806 --- /dev/null +++ b/internal/gui/view/convertor/encoders/encoders.go @@ -0,0 +1,15 @@ +package encoders + +import ( + "fyne.io/fyne/v2/widget" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/gui/view/convertor/encoders/h264_nvenc" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/gui/view/convertor/encoders/libx264" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/gui/view/convertor/encoders/libx265" +) + +var Views = map[string]func(encoder encoder.EncoderContract) []*widget.FormItem{ + "libx264": libx264.View, + "h264_nvenc": h264_nvenc.View, + "libx265": libx265.View, +} diff --git a/main.go b/main.go index 95a2e04..9250ddb 100644 --- a/main.go +++ b/main.go @@ -20,12 +20,21 @@ func main() { } app.SetMetadata(appMetadata) fyneApp := app.New() - progressBarService := application.NewProgressBar() + progressBarService := convertor.NewProgressBar() appSetting := setting.NewSetting(fyneApp) ffmpegService := ffmpeg.NewUtilities(appSetting) + convertorService := convertor.NewConvertor(ffmpegService) itemsToConvert := convertor.NewItemsToConvert(ffmpegService) queue := convertor.NewQueueList() - myApp := application.NewApp(fyneApp, appSetting, progressBarService, ffmpegService, itemsToConvert, queue) + myApp := application.NewApp( + fyneApp, + appSetting, + progressBarService, + ffmpegService, + itemsToConvert, + queue, + convertorService, + ) mainController := controller.NewController(myApp) mainController.Start() myApp.Run() -- 2.47.2 From 9bb835beaf4dbc88f795ff6c38ba7efe04130ac2 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sun, 8 Jun 2025 17:26:49 +0500 Subject: [PATCH 11/41] Make a choice of the encoder in the form Made a choice of the encoder by categories: video, audio and photo. --- internal/controller/convertor.go | 7 + internal/gui/view/convertor.go | 138 +++++++++++++++--- .../convertor/encoders/h264_nvenc/view.go | 64 ++++++++ .../view/convertor/encoders/libx264/view.go | 64 ++++++++ .../view/convertor/encoders/libx265/view.go | 64 ++++++++ internal/gui/window/layout.go | 3 +- internal/gui/window/main.go | 5 +- 7 files changed, 322 insertions(+), 23 deletions(-) create mode 100644 internal/gui/view/convertor/encoders/h264_nvenc/view.go create mode 100644 internal/gui/view/convertor/encoders/libx264/view.go create mode 100644 internal/gui/view/convertor/encoders/libx265/view.go diff --git a/internal/controller/convertor.go b/internal/controller/convertor.go index 898e3fd..aae050a 100644 --- a/internal/controller/convertor.go +++ b/internal/controller/convertor.go @@ -7,11 +7,18 @@ import ( ) func (c *controller) convertor() { + formats, err := c.app.GetConvertorService().GetSupportFormats() + if err != nil { + c.startWithError(err) + return + } + content := view.Convertor( c.window, c.addFileForConversion, c.app.GetSetting().GetDirectoryForSaving(), c.setDirectoryForSaving, + formats, ) c.window.SetContent(content) } diff --git a/internal/gui/view/convertor.go b/internal/gui/view/convertor.go index 8f52592..37e70ff 100644 --- a/internal/gui/view/convertor.go +++ b/internal/gui/view/convertor.go @@ -1,17 +1,18 @@ package view import ( - "errors" "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/lang" "fyne.io/fyne/v2/storage" "fyne.io/fyne/v2/widget" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application/convertor/encoder" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg" + encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/gui/view/convertor/encoders" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/gui/window" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/utils" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" "image/color" "os" "path/filepath" @@ -22,12 +23,14 @@ func Convertor( addFileForConversion func(file ffmpeg.File), directoryForSavingPath string, directoryForSaving func(path string), + formats encoder.ConvertorFormatsContract, ) fyne.CanvasObject { form := newFormConvertor( window, addFileForConversion, directoryForSavingPath, directoryForSaving, + formats, ) converterVideoFilesTitle := lang.L("converterVideoFilesTitle") @@ -48,6 +51,7 @@ func newFormConvertor( addFileForConversion func(file ffmpeg.File), directoryForSavingPath string, directoryForSaving func(path string), + formats encoder.ConvertorFormatsContract, ) *formConvertor { f := widget.NewForm() f.SubmitText = lang.L("converterVideoFilesSubmitTitle") @@ -61,6 +65,12 @@ func newFormConvertor( fileForConversion := formConvertor.newFileForConversion() directoryForSavingButton := formConvertor.newDirectoryForSaving(directoryForSavingPath) + isOverwriteOutputFiles := false + checkboxOverwriteOutputFiles := widget.NewCheck(lang.L("checkboxOverwriteOutputFilesTitle"), func(b bool) { + isOverwriteOutputFiles = b + }) + checkboxOverwriteOutputFiles.SetChecked(isOverwriteOutputFiles) + selectEncoder := formConvertor.newSelectEncoder(formats) items := []*widget.FormItem{ { @@ -78,9 +88,25 @@ func newFormConvertor( { Widget: container.NewHScroll(directoryForSavingButton.message), }, + + { + Widget: checkboxOverwriteOutputFiles, + }, + { + Widget: selectEncoder.SelectFileType, + }, + { + Text: lang.L("selectFormat"), + Widget: selectEncoder.SelectFormat, + }, + { + Text: lang.L("selectEncoder"), + Widget: selectEncoder.SelectEncoder, + }, } formConvertor.form.Items = items formConvertor.items = items + formConvertor.changeEncoder(selectEncoder.Encoder) return formConvertor } @@ -92,17 +118,13 @@ func (f *formConvertor) getForm() *widget.Form { type fileForConversion struct { button *widget.Button message *canvas.Text - file *kernel.File - - changeCallbacks map[int]func(err error) + file *ffmpeg.File } func (f *formConvertor) newFileForConversion() *fileForConversion { message := canvas.NewText("", color.RGBA{R: 255, G: 0, B: 0, A: 255}) fileForConversion := &fileForConversion{ message: message, - - changeCallbacks: map[int]func(err error){}, } buttonTitle := lang.L("choose") + "\n" + @@ -123,7 +145,6 @@ func (f *formConvertor) newFileForConversion() *fileForConversion { fileForConversion.message.Text = err.Error() fileForConversion.message.Refresh() }) - fileForConversion.eventSelectFile(err) return } if r == nil { @@ -136,8 +157,6 @@ func (f *formConvertor) newFileForConversion() *fileForConversion { Ext: r.URI().Extension(), }) - fileForConversion.eventSelectFile(nil) - listableURI := storage.NewFileURI(filepath.Dir(r.URI().Path())) locationURI, _ = storage.ListerForURI(listableURI) }, locationURI) @@ -166,8 +185,6 @@ func (f *formConvertor) newFileForConversion() *fileForConversion { Ext: uri.Extension(), }) - fileForConversion.eventSelectFile(nil) - listableURI := storage.NewFileURI(filepath.Dir(uri.Path())) locationURI, _ = storage.ListerForURI(listableURI) } @@ -175,7 +192,6 @@ func (f *formConvertor) newFileForConversion() *fileForConversion { if isError { fileForConversion.message.Text = lang.L("errorDragAndDropFile") utils.SetStringErrorStyle(fileForConversion.message) - fileForConversion.eventSelectFile(errors.New(fileForConversion.message.Text)) } else { fyne.Do(func() { fileForConversion.message.Text = "" @@ -238,12 +254,98 @@ func (f *formConvertor) newDirectoryForSaving(directoryForSavingPath string) *di return directoryForSaving } -func (c *fileForConversion) addChangeCallback(callback func(err error)) { - c.changeCallbacks[len(c.changeCallbacks)] = callback +type selectEncoder struct { + SelectFileType *widget.RadioGroup + SelectFormat *widget.Select + SelectEncoder *widget.Select + Encoder encoder2.EncoderContract } -func (c *fileForConversion) eventSelectFile(err error) { - for _, changeCallback := range c.changeCallbacks { - changeCallback(err) +func (f *formConvertor) newSelectEncoder(formats encoder.ConvertorFormatsContract) *selectEncoder { + selectEncoder := &selectEncoder{} + + encoderMap := map[int]encoder2.EncoderDataContract{} + selectEncoder.SelectEncoder = widget.NewSelect([]string{}, func(s string) { + if encoderMap[selectEncoder.SelectEncoder.SelectedIndex()] == nil { + return + } + selectEncoderData := encoderMap[selectEncoder.SelectEncoder.SelectedIndex()] + selectEncoder.Encoder = selectEncoderData.NewEncoder() + f.changeEncoder(selectEncoder.Encoder) + }) + + formatSelected := "" + selectEncoder.SelectFormat = widget.NewSelect([]string{}, func(s string) { + if formatSelected == s { + return + } + formatSelected = s + format, err := formats.GetFormat(s) + if err != nil { + return + } + var encoderOptions []string + encoderMap = map[int]encoder2.EncoderDataContract{} + for _, e := range format.GetEncoders() { + encoderMap[len(encoderMap)] = e + encoderOptions = append(encoderOptions, lang.L("encoder_"+e.GetTitle())) + } + selectEncoder.SelectEncoder.SetOptions(encoderOptions) + selectEncoder.SelectEncoder.SetSelectedIndex(0) + }) + + var fileTypeOptions []string + for _, fileType := range encoder2.GetListFileType() { + fileTypeOptions = append(fileTypeOptions, fileType.Name()) } + + encoderGroupVideo := lang.L("encoderGroupVideo") + encoderGroupAudio := lang.L("encoderGroupAudio") + encoderGroupImage := lang.L("encoderGroupImage") + encoderGroup := map[string]string{ + encoderGroupVideo: "video", + encoderGroupAudio: "audio", + encoderGroupImage: "image", + } + selectEncoder.SelectFileType = widget.NewRadioGroup([]string{encoderGroupVideo, encoderGroupAudio, encoderGroupImage}, func(s string) { + groupCode := encoderGroup[s] + + var formatOptions []string + for _, f := range formats.GetFormats() { + if groupCode != f.GetFileType().Name() { + continue + } + formatOptions = append(formatOptions, f.GetTitle()) + } + selectEncoder.SelectFormat.SetOptions(formatOptions) + if groupCode == encoder2.FileType(encoder2.Video).Name() { + selectEncoder.SelectFormat.SetSelected("mp4") + } else { + selectEncoder.SelectFormat.SetSelectedIndex(0) + } + }) + selectEncoder.SelectFileType.Horizontal = true + selectEncoder.SelectFileType.Required = true + selectEncoder.SelectFileType.SetSelected(encoderGroupVideo) + + return selectEncoder +} + +func (f *formConvertor) changeEncoder(encoder encoder2.EncoderContract) { + var items []*widget.FormItem + + if encoders.Views[encoder.GetName()] != nil { + items = encoders.Views[encoder.GetName()](encoder) + } + + f.changeItems(items) +} + +func (f *formConvertor) changeItems(items []*widget.FormItem) { + fyne.Do(func() { + f.form.Items = f.items + f.form.Refresh() + f.form.Items = append(f.form.Items, items...) + f.form.Refresh() + }) } diff --git a/internal/gui/view/convertor/encoders/h264_nvenc/view.go b/internal/gui/view/convertor/encoders/h264_nvenc/view.go new file mode 100644 index 0000000..a506a12 --- /dev/null +++ b/internal/gui/view/convertor/encoders/h264_nvenc/view.go @@ -0,0 +1,64 @@ +package h264_nvenc + +import ( + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/lang" + "fyne.io/fyne/v2/widget" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/h264_nvenc" +) + +func View(encoder encoder.EncoderContract) []*widget.FormItem { + items := []*widget.FormItem{} + + items = append(items, presetParameter(encoder)...) + + return items +} + +func presetParameter(encoder encoder.EncoderContract) []*widget.FormItem { + parameter, err := encoder.GetParameter("preset") + if err != nil { + return nil + } + + presets := map[string]string{} + presetsForSelect := []string{} + presetDefault := "" + + for _, name := range h264_nvenc.Presets { + title := name + presetsForSelect = append(presetsForSelect, name) + presets[title] = name + if name == parameter.Get() { + presetDefault = title + } + } + + elementSelect := widget.NewSelect(presetsForSelect, func(s string) { + if presets[s] == "" { + return + } + parameter.Set(presets[s]) + }) + elementSelect.SetSelected(presetDefault) + elementSelect.Hide() + + checkboxTitle := lang.L("parameterCheckbox") + elementCheckbox := widget.NewCheck(checkboxTitle, func(b bool) { + if b == true { + parameter.SetEnable() + elementSelect.Show() + return + } + parameter.SetDisable() + elementSelect.Hide() + }) + + return []*widget.FormItem{ + { + Text: lang.L("formPreset"), + Widget: container.NewVBox(elementCheckbox, elementSelect), + }, + } +} diff --git a/internal/gui/view/convertor/encoders/libx264/view.go b/internal/gui/view/convertor/encoders/libx264/view.go new file mode 100644 index 0000000..8457d94 --- /dev/null +++ b/internal/gui/view/convertor/encoders/libx264/view.go @@ -0,0 +1,64 @@ +package libx264 + +import ( + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/lang" + "fyne.io/fyne/v2/widget" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/libx264" +) + +func View(encoder encoder.EncoderContract) []*widget.FormItem { + items := []*widget.FormItem{} + + items = append(items, presetParameter(encoder)...) + + return items +} + +func presetParameter(encoder encoder.EncoderContract) []*widget.FormItem { + parameter, err := encoder.GetParameter("preset") + if err != nil { + return nil + } + + presets := map[string]string{} + presetsForSelect := []string{} + presetDefault := "" + + for _, name := range libx264.Presets { + title := lang.L("preset_" + name) + presetsForSelect = append(presetsForSelect, title) + presets[title] = name + if name == parameter.Get() { + presetDefault = title + } + } + + elementSelect := widget.NewSelect(presetsForSelect, func(s string) { + if presets[s] == "" { + return + } + parameter.Set(presets[s]) + }) + elementSelect.SetSelected(presetDefault) + elementSelect.Hide() + + checkboxTitle := lang.L("parameterCheckbox") + elementCheckbox := widget.NewCheck(checkboxTitle, func(b bool) { + if b == true { + parameter.SetEnable() + elementSelect.Show() + return + } + parameter.SetDisable() + elementSelect.Hide() + }) + + return []*widget.FormItem{ + { + Text: lang.L("formPreset"), + Widget: container.NewVBox(elementCheckbox, elementSelect), + }, + } +} diff --git a/internal/gui/view/convertor/encoders/libx265/view.go b/internal/gui/view/convertor/encoders/libx265/view.go new file mode 100644 index 0000000..f0fccf0 --- /dev/null +++ b/internal/gui/view/convertor/encoders/libx265/view.go @@ -0,0 +1,64 @@ +package libx265 + +import ( + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/lang" + "fyne.io/fyne/v2/widget" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/encoder/libx265" +) + +func View(encoder encoder.EncoderContract) []*widget.FormItem { + items := []*widget.FormItem{} + + items = append(items, presetParameter(encoder)...) + + return items +} + +func presetParameter(encoder encoder.EncoderContract) []*widget.FormItem { + parameter, err := encoder.GetParameter("preset") + if err != nil { + return nil + } + + presets := map[string]string{} + presetsForSelect := []string{} + presetDefault := "" + + for _, name := range libx265.Presets { + title := lang.L("preset_" + name) + presetsForSelect = append(presetsForSelect, title) + presets[title] = name + if name == parameter.Get() { + presetDefault = title + } + } + + elementSelect := widget.NewSelect(presetsForSelect, func(s string) { + if presets[s] == "" { + return + } + parameter.Set(presets[s]) + }) + elementSelect.SetSelected(presetDefault) + elementSelect.Hide() + + checkboxTitle := lang.L("parameterCheckbox") + elementCheckbox := widget.NewCheck(checkboxTitle, func(b bool) { + if b == true { + parameter.SetEnable() + elementSelect.Show() + return + } + parameter.SetDisable() + elementSelect.Hide() + }) + + return []*widget.FormItem{ + { + Text: lang.L("formPreset"), + Widget: container.NewVBox(elementCheckbox, elementSelect), + }, + } +} diff --git a/internal/gui/window/layout.go b/internal/gui/window/layout.go index b5423cd..33e4b25 100644 --- a/internal/gui/window/layout.go +++ b/internal/gui/window/layout.go @@ -7,7 +7,6 @@ import ( "fyne.io/fyne/v2/lang" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application/convertor" ) @@ -21,7 +20,7 @@ type layout struct { rContainer RightMainContainerContract } -func NewLayout(progressBarService application.ProgressBarContract, itemsToConvert convertor.ItemsToConvertContract, queueLayout QueueLayoutContract) LayoutContract { +func NewLayout(progressBarService convertor.ProgressBarContract, itemsToConvert convertor.ItemsToConvertContract, queueLayout QueueLayoutContract) LayoutContract { rContainer := newRightContainer(progressBarService.GetContainer(), itemsToConvert, queueLayout) layoutContainer := container.NewAdaptiveGrid(2, widget.NewLabel(""), rContainer.GetCanvasObject()) diff --git a/internal/gui/window/main.go b/internal/gui/window/main.go index 990c9d0..98ff5f4 100644 --- a/internal/gui/window/main.go +++ b/internal/gui/window/main.go @@ -3,7 +3,6 @@ package window import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/dialog" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application/convertor" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/utils" ) @@ -22,13 +21,13 @@ type mainWindow struct { fyneWindow fyne.Window layout LayoutContract itemsToConvert convertor.ItemsToConvertContract - progressBarService application.ProgressBarContract + progressBarService convertor.ProgressBarContract queueLayout QueueLayoutContract } func NewMainWindow( fyneWindow fyne.Window, - progressBarService application.ProgressBarContract, + progressBarService convertor.ProgressBarContract, itemsToConvert convertor.ItemsToConvertContract, queueLayout QueueLayoutContract, ) WindowContract { -- 2.47.2 From e48f363de02546a0e64e3af0ddb60bfa7d5ca0fc Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sun, 8 Jun 2025 18:30:08 +0500 Subject: [PATCH 12/41] Fix the bug where the file selection button for conversion disappears. --- internal/gui/view/convertor.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/gui/view/convertor.go b/internal/gui/view/convertor.go index 37e70ff..eb7b2b9 100644 --- a/internal/gui/view/convertor.go +++ b/internal/gui/view/convertor.go @@ -107,6 +107,7 @@ func newFormConvertor( formConvertor.form.Items = items formConvertor.items = items formConvertor.changeEncoder(selectEncoder.Encoder) + selectEncoder.ChangeEncoder = formConvertor.changeEncoder return formConvertor } @@ -259,6 +260,8 @@ type selectEncoder struct { SelectFormat *widget.Select SelectEncoder *widget.Select Encoder encoder2.EncoderContract + + ChangeEncoder func(encoder encoder2.EncoderContract) } func (f *formConvertor) newSelectEncoder(formats encoder.ConvertorFormatsContract) *selectEncoder { @@ -271,7 +274,9 @@ func (f *formConvertor) newSelectEncoder(formats encoder.ConvertorFormatsContrac } selectEncoderData := encoderMap[selectEncoder.SelectEncoder.SelectedIndex()] selectEncoder.Encoder = selectEncoderData.NewEncoder() - f.changeEncoder(selectEncoder.Encoder) + if selectEncoder.ChangeEncoder != nil { + selectEncoder.ChangeEncoder(selectEncoder.Encoder) + } }) formatSelected := "" -- 2.47.2 From df8095fb169032ec65a53460c1f7b6c3255b7e8b Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sun, 8 Jun 2025 19:19:50 +0500 Subject: [PATCH 13/41] Added an action to the submit button. --- internal/gui/view/convertor.go | 65 +++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/internal/gui/view/convertor.go b/internal/gui/view/convertor.go index eb7b2b9..7eb722d 100644 --- a/internal/gui/view/convertor.go +++ b/internal/gui/view/convertor.go @@ -18,19 +18,33 @@ import ( "path/filepath" ) +type ConvertSetting struct { + DirectoryForSave string + OverwriteOutputFiles bool + Format string + Encoder encoder2.EncoderContract +} + func Convertor( window window.WindowContract, addFileForConversion func(file ffmpeg.File), directoryForSavingPath string, directoryForSaving func(path string), formats encoder.ConvertorFormatsContract, + addToConversion func(convertSetting ConvertSetting) error, ) fyne.CanvasObject { + conversionMessage := canvas.NewText("", color.RGBA{R: 255, G: 0, B: 0, A: 255}) + conversionMessage.TextSize = 16 + conversionMessage.TextStyle = fyne.TextStyle{Bold: true} + form := newFormConvertor( window, addFileForConversion, directoryForSavingPath, directoryForSaving, formats, + addToConversion, + conversionMessage, ) converterVideoFilesTitle := lang.L("converterVideoFilesTitle") @@ -38,8 +52,9 @@ func Convertor( } type formConvertor struct { - form *widget.Form - items []*widget.FormItem + form *widget.Form + items []*widget.FormItem + conversionMessage *canvas.Text window window.WindowContract addFileForConversion func(file ffmpeg.File) @@ -52,6 +67,8 @@ func newFormConvertor( directoryForSavingPath string, directoryForSaving func(path string), formats encoder.ConvertorFormatsContract, + addToConversion func(convertSetting ConvertSetting) error, + conversionMessage *canvas.Text, ) *formConvertor { f := widget.NewForm() f.SubmitText = lang.L("converterVideoFilesSubmitTitle") @@ -61,6 +78,7 @@ func newFormConvertor( window: window, addFileForConversion: addFileForConversion, directoryForSaving: directoryForSaving, + conversionMessage: conversionMessage, } fileForConversion := formConvertor.newFileForConversion() @@ -109,11 +127,50 @@ func newFormConvertor( formConvertor.changeEncoder(selectEncoder.Encoder) selectEncoder.ChangeEncoder = formConvertor.changeEncoder + formConvertor.form.OnSubmit = func() { + formConvertor.conversionMessage.Text = "" + if len(directoryForSavingButton.path) == 0 { + formConvertor.conversionMessage.Text = lang.L("errorSelectedFolderSave") + return + } + + if len(selectEncoder.SelectFormat.Selected) == 0 { + formConvertor.conversionMessage.Text = lang.L("errorSelectedFormat") + return + } + + if selectEncoder.Encoder == nil { + formConvertor.conversionMessage.Text = lang.L("errorSelectedEncoder") + return + } + + fileForConversion.button.Disable() + directoryForSavingButton.button.Disable() + formConvertor.form.Disable() + + err := addToConversion(ConvertSetting{ + DirectoryForSave: directoryForSavingButton.path, + OverwriteOutputFiles: isOverwriteOutputFiles, + Format: selectEncoder.SelectFormat.Selected, + Encoder: selectEncoder.Encoder, + }) + if err != nil { + formConvertor.conversionMessage.Text = err.Error() + } + + fileForConversion.button.Enable() + directoryForSavingButton.button.Enable() + formConvertor.form.Enable() + } + return formConvertor } -func (f *formConvertor) getForm() *widget.Form { - return f.form +func (f *formConvertor) getForm() fyne.CanvasObject { + return container.NewVBox( + f.form, + container.NewHScroll(f.conversionMessage), + ) } type fileForConversion struct { -- 2.47.2 From 29ca392880057ac20f94e75a856517548de20ed2 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sun, 8 Jun 2025 20:42:43 +0500 Subject: [PATCH 14/41] Made it so that files for conversion are added to the queue. --- internal/application/convertor/queue.go | 1 + internal/controller/convertor.go | 31 ++++++ internal/controller/main.go | 2 +- internal/ffmpeg/utilities.go | 4 +- internal/gui/view/convertor.go | 25 ++--- internal/gui/window/queue.go | 129 ++++++++++++++++++++++-- 6 files changed, 171 insertions(+), 21 deletions(-) diff --git a/internal/application/convertor/queue.go b/internal/application/convertor/queue.go index d15beb6..455009c 100644 --- a/internal/application/convertor/queue.go +++ b/internal/application/convertor/queue.go @@ -67,6 +67,7 @@ func NewQueueList() QueueListContract { return &queueList{ currentKey: 0, items: map[int]*Queue{}, + queue: map[int]int{}, queueListener: map[int]QueueListenerContract{}, } } diff --git a/internal/controller/convertor.go b/internal/controller/convertor.go index aae050a..445a32d 100644 --- a/internal/controller/convertor.go +++ b/internal/controller/convertor.go @@ -1,9 +1,12 @@ package controller import ( + "errors" + "fyne.io/fyne/v2/lang" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg/download/service" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/gui/view" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/utils" ) func (c *controller) convertor() { @@ -19,6 +22,7 @@ func (c *controller) convertor() { c.app.GetSetting().GetDirectoryForSaving(), c.setDirectoryForSaving, formats, + c.addToConversion, ) c.window.SetContent(content) } @@ -32,6 +36,33 @@ func (c *controller) setDirectoryForSaving(path string) { c.app.GetSetting().SetDirectoryForSaving(path) } +func (c *controller) addToConversion(convertSetting view.ConvertSetting) error { + if len(c.app.GetItemsToConvert().GetItems()) == 0 { + return errors.New(lang.L("errorNoFilesAddedForConversion")) + } + c.window.GetLayout().GetRContainer().SelectFileQueueTab() + for _, item := range c.app.GetItemsToConvert().GetItems() { + file := item.GetFile() + if file == nil { + continue + } + + c.app.GetQueueService().Add(&ffmpeg.ConvertSetting{ + FileInput: *file, + FileOut: ffmpeg.File{ + Path: convertSetting.DirectoryForSave + utils.PathSeparator() + file.Name + "." + convertSetting.Format, + Name: file.Name, + Ext: "." + convertSetting.Format, + }, + OverwriteOutputFiles: convertSetting.OverwriteOutputFiles, + Encoder: convertSetting.Encoder, + }) + } + c.app.GetItemsToConvert().AfterAddingQueue() + + return nil +} + func (c *controller) settingConvertor(isAllowCancellation bool) { ffmpegPath := c.app.GetFFmpegService().GetFFmpegPath() ffprobePath := c.app.GetFFmpegService().GetFFprobePath() diff --git a/internal/controller/main.go b/internal/controller/main.go index db86488..78dff67 100644 --- a/internal/controller/main.go +++ b/internal/controller/main.go @@ -18,7 +18,7 @@ type controller struct { func NewController(app application.AppContract) ControllerContract { fyneWindow := app.FyneApp().NewWindow(app.FyneApp().Metadata().Name) - queueLayout := window.NewQueueLayout() + queueLayout := window.NewQueueLayout(app.GetFFmpegService()) app.GetQueueService().AddListener(queueLayout) return &controller{ diff --git a/internal/ffmpeg/utilities.go b/internal/ffmpeg/utilities.go index 5c7c891..214f862 100644 --- a/internal/ffmpeg/utilities.go +++ b/internal/ffmpeg/utilities.go @@ -14,8 +14,8 @@ type File struct { } type ConvertSetting struct { - VideoFileInput File - VideoFileOut File + FileInput File + FileOut File OverwriteOutputFiles bool Encoder encoder.EncoderContract } diff --git a/internal/gui/view/convertor.go b/internal/gui/view/convertor.go index 7eb722d..cac1bc2 100644 --- a/internal/gui/view/convertor.go +++ b/internal/gui/view/convertor.go @@ -148,19 +148,20 @@ func newFormConvertor( directoryForSavingButton.button.Disable() formConvertor.form.Disable() - err := addToConversion(ConvertSetting{ - DirectoryForSave: directoryForSavingButton.path, - OverwriteOutputFiles: isOverwriteOutputFiles, - Format: selectEncoder.SelectFormat.Selected, - Encoder: selectEncoder.Encoder, + fyne.Do(func() { + err := addToConversion(ConvertSetting{ + DirectoryForSave: directoryForSavingButton.path, + OverwriteOutputFiles: isOverwriteOutputFiles, + Format: selectEncoder.SelectFormat.Selected, + Encoder: selectEncoder.Encoder, + }) + if err != nil { + formConvertor.conversionMessage.Text = err.Error() + } + fileForConversion.button.Enable() + directoryForSavingButton.button.Enable() + formConvertor.form.Enable() }) - if err != nil { - formConvertor.conversionMessage.Text = err.Error() - } - - fileForConversion.button.Enable() - directoryForSavingButton.button.Enable() - formConvertor.form.Enable() } return formConvertor diff --git a/internal/gui/window/queue.go b/internal/gui/window/queue.go index 31be767..9bcb4c0 100644 --- a/internal/gui/window/queue.go +++ b/internal/gui/window/queue.go @@ -4,8 +4,12 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/lang" + "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application/convertor" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg" + "image/color" "strconv" "strings" ) @@ -23,15 +27,17 @@ type queueLayout struct { itemsContainer *fyne.Container queueAllStatistics QueueStatisticsAllContract items map[int]queueLayoutItem + ffmpegService ffmpeg.UtilitiesContract } -func NewQueueLayout() QueueLayoutContract { +func NewQueueLayout(ffmpegService ffmpeg.UtilitiesContract) QueueLayoutContract { items := map[int]queueLayoutItem{} return &queueLayout{ itemsContainer: container.NewVBox(), queueAllStatistics: newQueueAllStatistics(&items), items: items, + ffmpegService: ffmpegService, } } @@ -43,16 +49,92 @@ func (l *queueLayout) GetQueueStatistics() QueueStatisticsAllContract { return l.queueAllStatistics } -func (l *queueLayout) AddQueue(key int, queue *convertor.Queue) { +func (l *queueLayout) AddQueue(queueID int, queue *convertor.Queue) { + + statusMessage := canvas.NewText(l.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.FileInput.Name)), + container.NewHBox( + buttonPlay, + statusMessage, + ), + blockMessageError, + container.NewPadded(), + canvas.NewLine(theme.Color(theme.ColorNameFocus)), + container.NewPadded(), + ) + l.addQueueStatistics() + if l.GetQueueStatistics().IsChecked(queue.Status) == false { + content.Hide() + } + + l.items[queueID] = queueLayoutItem{ + CanvasObject: content, + StatusMessage: statusMessage, + BlockMessageError: blockMessageError, + MessageError: messageError, + buttonPlay: buttonPlay, + status: queue.Status, + } + l.itemsContainer.Add(content) } -func (l *queueLayout) ChangeQueue(key int, queue *convertor.Queue) { - l.changeQueueStatistics(queue.Status) +func (l *queueLayout) ChangeQueue(queueID int, queue *convertor.Queue) { + if item, ok := l.items[queueID]; ok { + statusColor := l.getStatusColor(queue.Status) + fyne.Do(func() { + item.StatusMessage.Text = l.getStatusTitle(queue.Status) + item.StatusMessage.Color = statusColor + item.StatusMessage.Refresh() + }) + if queue.Error != nil { + fyne.Do(func() { + item.MessageError.Text = queue.Error.Error() + item.MessageError.Color = statusColor + item.BlockMessageError.Show() + item.MessageError.Refresh() + }) + } + if queue.Status == convertor.StatusType(convertor.Completed) { + item.buttonPlay.Show() + item.buttonPlay.OnTapped = func() { + item.buttonPlay.Disable() + go func() { + ffplay, err := l.ffmpegService.GetFFplay() + if err == nil { + _ = ffplay.Play(&queue.Setting.FileOut) + } + fyne.Do(func() { + item.buttonPlay.Enable() + }) + }() + } + } + if l.GetQueueStatistics().IsChecked(queue.Status) == false && item.CanvasObject.Visible() == true { + item.CanvasObject.Hide() + } else if item.CanvasObject.Visible() == false { + item.CanvasObject.Show() + } + + l.changeQueueStatistics(queue.Status) + } } -func (l *queueLayout) RemoveQueue(key int, status convertor.StatusContract) { - l.removeQueueStatistics(status) +func (l *queueLayout) RemoveQueue(queueID int, status convertor.StatusContract) { + if item, ok := l.items[queueID]; ok { + l.itemsContainer.Remove(item.CanvasObject) + l.removeQueueStatistics(status) + l.items[queueID] = queueLayoutItem{} + } } func (l *queueLayout) addQueueStatistics() { @@ -104,12 +186,30 @@ func (l *queueLayout) removeQueueStatistics(status convertor.StatusContract) { } } +func (l *queueLayout) getStatusTitle(status convertor.StatusContract) string { + return lang.L(status.Name() + "Queue") +} + +func (l *queueLayout) getStatusColor(status convertor.StatusContract) color.Color { + if status == convertor.StatusType(convertor.Error) { + return theme.Color(theme.ColorNameError) + } + + if status == convertor.StatusType(convertor.Completed) { + return color.RGBA{R: 49, G: 127, B: 114, A: 255} + } + + return theme.Color(theme.ColorNamePrimary) +} + type QueueStatisticsAllContract interface { GetWaiting() QueueStatisticsContract GetInProgress() QueueStatisticsContract GetCompleted() QueueStatisticsContract GetError() QueueStatisticsContract GetTotal() QueueStatisticsContract + + IsChecked(status convertor.StatusContract) bool } type queueAllStatistics struct { @@ -203,6 +303,23 @@ func (s *queueAllStatistics) GetTotal() QueueStatisticsContract { return s.total } +func (s *queueAllStatistics) IsChecked(status convertor.StatusContract) bool { + if status == convertor.StatusType(convertor.InProgress) { + return s.inProgress.GetCheckbox().Checked + } + if status == convertor.StatusType(convertor.Completed) { + return s.completed.GetCheckbox().Checked + } + if status == convertor.StatusType(convertor.Error) { + return s.error.GetCheckbox().Checked + } + if status == convertor.StatusType(convertor.Waiting) { + return s.waiting.GetCheckbox().Checked + } + + return true +} + func (s *queueAllStatistics) redrawingQueueItems(queueItems *map[int]queueLayoutItem) { for _, item := range *queueItems { if s.isChecked(item.status) == true && item.CanvasObject.Visible() == false { -- 2.47.2 From eb43669ae7123f5082c698d008552da941217469 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sun, 8 Jun 2025 21:38:29 +0500 Subject: [PATCH 15/41] Add `GetTotalDuration` method to `FFprobeContract` and implementation Extend `FFprobeContract` interface with `GetTotalDuration` for retrieving the duration of media files. --- internal/ffmpeg/ffprobe.go | 59 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/internal/ffmpeg/ffprobe.go b/internal/ffmpeg/ffprobe.go index 8aa18fc..ac8c2f9 100644 --- a/internal/ffmpeg/ffprobe.go +++ b/internal/ffmpeg/ffprobe.go @@ -5,11 +5,14 @@ import ( "fyne.io/fyne/v2/lang" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/utils" "os/exec" + "strconv" "strings" + "unicode" ) type FFprobeContract interface { GetPath() string + GetTotalDuration(file *File) (float64, error) } type ffprobe struct { @@ -38,6 +41,50 @@ func (f *ffprobe) GetPath() string { return f.path } +func (f *ffprobe) GetTotalDuration(file *File) (duration float64, err error) { + args := []string{"-v", "error", "-select_streams", "v:0", "-count_packets", "-show_entries", "stream=nb_read_packets", "-of", "csv=p=0", file.Path} + cmd := exec.Command(f.path, args...) + utils.PrepareBackgroundCommand(cmd) + out, err := cmd.CombinedOutput() + if err != nil { + errString := strings.TrimSpace(string(out)) + if len(errString) > 1 { + return 0, errors.New(errString) + } + return 0, err + } + frames := strings.TrimSpace(string(out)) + if len(frames) == 0 { + return f.getAlternativeTotalDuration(file) + } + + duration, err = strconv.ParseFloat(frames, 64) + if err != nil { + // fix .mts duration + return strconv.ParseFloat(getFirstDigits(frames), 64) + } + return duration, err +} + +func (f *ffprobe) getAlternativeTotalDuration(file *File) (duration float64, err error) { + args := []string{"-v", "error", "-select_streams", "a:0", "-count_packets", "-show_entries", "stream=nb_read_packets", "-of", "csv=p=0", file.Path} + cmd := exec.Command(f.path, args...) + utils.PrepareBackgroundCommand(cmd) + out, err := cmd.CombinedOutput() + if err != nil { + errString := strings.TrimSpace(string(out)) + if len(errString) > 1 { + return 0, errors.New(errString) + } + return 0, err + } + frames := strings.TrimSpace(string(out)) + if len(frames) == 0 { + return 0, errors.New("error getting number of frames") + } + return strconv.ParseFloat(frames, 64) +} + func checkFFprobePath(path string) (bool, error) { cmd := exec.Command(path, "-version") utils.PrepareBackgroundCommand(cmd) @@ -50,3 +97,15 @@ func checkFFprobePath(path string) (bool, error) { } return true, nil } + +func getFirstDigits(s string) string { + result := "" + for _, r := range s { + if unicode.IsDigit(r) { + result += string(r) + } else { + break + } + } + return result +} -- 2.47.2 From 1b1cdd5c22c57574cb98433469d012c57a7f7b35 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sun, 8 Jun 2025 21:47:28 +0500 Subject: [PATCH 16/41] Add `RunConvert` method to `FFmpegContract` and implementation Extend `FFmpegContract` with `RunConvert` for handling file conversion, including progress tracking and callback support before and after execution. --- internal/ffmpeg/ffmpeg.go | 52 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index db5a30d..b303182 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -5,13 +5,20 @@ import ( "errors" "fyne.io/fyne/v2/lang" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/utils" + "io" "os/exec" "strings" ) +type ProgressContract interface { + GetProtocole() string + Run(stdOut io.ReadCloser, stdErr io.ReadCloser) error +} + type FFmpegContract interface { GetPath() string GetEncoders(scanner func(scanner *bufio.Reader)) error + RunConvert(setting ConvertSetting, progress ProgressContract, beforeWait func(cmd *exec.Cmd), afterWait func(cmd *exec.Cmd)) error } type ffmpeg struct { @@ -40,6 +47,51 @@ func (f *ffmpeg) GetPath() string { return f.path } +func (f *ffmpeg) RunConvert(setting ConvertSetting, progress ProgressContract, beforeWait func(cmd *exec.Cmd), afterWait func(cmd *exec.Cmd)) error { + overwriteOutputFiles := "-n" + if setting.OverwriteOutputFiles == true { + overwriteOutputFiles = "-y" + } + args := []string{overwriteOutputFiles, "-i", setting.FileInput.Path} + args = append(args, setting.Encoder.GetParams()...) + args = append(args, "-progress", progress.GetProtocole(), setting.FileOut.Path) + cmd := exec.Command(f.path, args...) + utils.PrepareBackgroundCommand(cmd) + + stdOut, err := cmd.StdoutPipe() + if err != nil { + return err + } + stdErr, err := cmd.StderrPipe() + if err != nil { + return err + } + + err = cmd.Start() + if err != nil { + return err + } + + if beforeWait != nil { + beforeWait(cmd) + } + + errProgress := progress.Run(stdOut, stdErr) + + err = cmd.Wait() + if afterWait != nil { + afterWait(cmd) + } + if errProgress != nil { + return errProgress + } + if err != nil { + return err + } + + return nil +} + func (f *ffmpeg) GetEncoders(scanner func(scanner *bufio.Reader)) error { cmd := exec.Command(f.path, "-encoders") utils.PrepareBackgroundCommand(cmd) -- 2.47.2 From 2909ef7cea2cc4cd5602ec14b468dd72437dd8e1 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sun, 8 Jun 2025 22:19:28 +0500 Subject: [PATCH 17/41] Introduce progress bar updates and queue processing logic Implemented progress bar integration with `ProgressBarContract` for real-time conversion tracking and status updates. Added queue management functionality to process files sequentially with error and completion handling. Extended `ConvertorContract` and `FFmpegContract` to support tracking of running processes and conversion progress. --- internal/application/app.go | 56 +++++++ internal/application/convertor/convertor.go | 37 ++++- internal/application/convertor/progressbar.go | 156 +++++++++++++++++- main.go | 6 +- 4 files changed, 251 insertions(+), 4 deletions(-) diff --git a/internal/application/app.go b/internal/application/app.go index 619a1e4..dc26bf3 100644 --- a/internal/application/app.go +++ b/internal/application/app.go @@ -5,6 +5,7 @@ import ( "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application/convertor" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application/setting" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg" + "time" ) type AppContract interface { @@ -16,6 +17,8 @@ type AppContract interface { GetItemsToConvert() convertor.ItemsToConvertContract GetQueueService() convertor.QueueListContract Run() + AfterClosing() + RunConvertor() } type application struct { @@ -79,3 +82,56 @@ func (a *application) GetConvertorService() convertor.ConvertorContract { func (a *application) Run() { a.fyneApp.Run() } + +func (a *application) RunConvertor() { + go func() { + for { + time.Sleep(time.Millisecond * 3000) + queueId, queue := a.queueService.Next() + if queue == nil { + continue + } + + queue.Status = convertor.StatusType(convertor.InProgress) + a.queueService.EventChangeQueue(queueId, queue) + + if a.progressBarService.GetContainer().Hidden { + a.progressBarService.GetContainer().Show() + } + + totalDuration := float64(0) + ffprobe, err := a.ffmpegService.GetFFprobe() + if err == nil { + totalDuration, err = ffprobe.GetTotalDuration(&queue.Setting.FileInput) + if err != nil { + totalDuration = float64(0) + } + } + + progress := a.progressBarService.GetProgressbar( + totalDuration, + queue.Setting.FileInput.Path, + ) + + err = a.convertorService.RunConvert(*queue.Setting, progress) + if err != nil { + queue.Status = convertor.StatusType(convertor.Error) + queue.Error = err + a.queueService.EventChangeQueue(queueId, queue) + a.progressBarService.ProcessEndedWithError(err.Error()) + + continue + } + + queue.Status = convertor.StatusType(convertor.Completed) + a.queueService.EventChangeQueue(queueId, queue) + a.progressBarService.ProcessEndedWithSuccess(&queue.Setting.FileOut) + } + }() +} + +func (a *application) AfterClosing() { + for _, cmd := range a.convertorService.GetRunningProcesses() { + _ = cmd.Process.Kill() + } +} diff --git a/internal/application/convertor/convertor.go b/internal/application/convertor/convertor.go index c36dc56..2cce78b 100644 --- a/internal/application/convertor/convertor.go +++ b/internal/application/convertor/convertor.go @@ -5,25 +5,54 @@ import ( "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application/convertor/encoder" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg" "io" + "os/exec" "strings" ) type ConvertorContract interface { + RunConvert(setting ffmpeg.ConvertSetting, progress ffmpeg.ProgressContract) error GetSupportFormats() (encoder.ConvertorFormatsContract, error) + GetRunningProcesses() map[int]*exec.Cmd +} + +type runningProcesses struct { + items map[int]*exec.Cmd + numberOfStarts int } type convertor struct { - ffmpegService ffmpeg.UtilitiesContract + ffmpegService ffmpeg.UtilitiesContract + runningProcesses *runningProcesses } func NewConvertor( ffmpegService ffmpeg.UtilitiesContract, ) ConvertorContract { return &convertor{ - ffmpegService: ffmpegService, + ffmpegService: ffmpegService, + runningProcesses: &runningProcesses{items: map[int]*exec.Cmd{}, numberOfStarts: 0}, } } +func (c *convertor) RunConvert(setting ffmpeg.ConvertSetting, progress ffmpeg.ProgressContract) error { + ffmpegService, err := c.ffmpegService.GetFFmpeg() + if err != nil { + return err + } + + index := c.runningProcesses.numberOfStarts + beforeWait := func(cmd *exec.Cmd) { + c.runningProcesses.numberOfStarts++ + c.runningProcesses.items[index] = cmd + } + + afterWait := func(cmd *exec.Cmd) { + delete(c.runningProcesses.items, index) + } + + return ffmpegService.RunConvert(setting, progress, beforeWait, afterWait) +} + func (c *convertor) GetSupportFormats() (encoder.ConvertorFormatsContract, error) { var err error @@ -52,3 +81,7 @@ func (c *convertor) GetSupportFormats() (encoder.ConvertorFormatsContract, error return formats, err } + +func (c *convertor) GetRunningProcesses() map[int]*exec.Cmd { + return c.runningProcesses.items +} diff --git a/internal/application/convertor/progressbar.go b/internal/application/convertor/progressbar.go index 173e994..30c8155 100644 --- a/internal/application/convertor/progressbar.go +++ b/internal/application/convertor/progressbar.go @@ -1,15 +1,27 @@ package convertor import ( + "bufio" + "errors" "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/lang" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg" + "image/color" + "io" + "regexp" + "strconv" + "strings" ) type ProgressBarContract interface { GetContainer() *fyne.Container + GetProgressbar(totalDuration float64, filePath string) ffmpeg.ProgressContract + ProcessEndedWithError(errorText string) + ProcessEndedWithSuccess(file *ffmpeg.File) } type progressBar struct { @@ -20,9 +32,10 @@ type progressBar struct { messageError *canvas.Text statusMessage *canvas.Text buttonPlay *widget.Button + ffmpegService ffmpeg.UtilitiesContract } -func NewProgressBar() ProgressBarContract { +func NewProgressBar(ffmpegService ffmpeg.UtilitiesContract) ProgressBarContract { label := widget.NewLabel("") progressbar := widget.NewProgressBar() @@ -55,9 +68,150 @@ func NewProgressBar() ProgressBarContract { messageError: messageError, statusMessage: statusMessage, buttonPlay: buttonPlay, + ffmpegService: ffmpegService, } } func (p *progressBar) GetContainer() *fyne.Container { return p.container } + +func (p *progressBar) GetProgressbar(totalDuration float64, filePath string) ffmpeg.ProgressContract { + p.label.Text = filePath + p.statusMessage.Color = theme.Color(theme.ColorNamePrimary) + p.statusMessage.Text = lang.L("inProgressQueue") + p.messageError.Text = "" + fyne.Do(func() { + p.buttonPlay.Hide() + if p.errorBlock.Visible() { + p.errorBlock.Hide() + } + p.statusMessage.Refresh() + p.container.Refresh() + p.errorBlock.Refresh() + }) + + p.progressbar.Value = 0 + return NewProgress(totalDuration, p.progressbar) +} + +func (p *progressBar) ProcessEndedWithError(errorText string) { + fyne.Do(func() { + p.statusMessage.Color = theme.Color(theme.ColorNameError) + p.statusMessage.Text = lang.L("errorQueue") + p.messageError.Text = errorText + p.errorBlock.Show() + }) +} + +func (p *progressBar) ProcessEndedWithSuccess(file *ffmpeg.File) { + fyne.Do(func() { + p.statusMessage.Color = color.RGBA{R: 49, G: 127, B: 114, A: 255} + p.statusMessage.Text = lang.L("completedQueue") + p.buttonPlay.Show() + p.buttonPlay.OnTapped = func() { + p.buttonPlay.Disable() + go func() { + ffplay, err := p.ffmpegService.GetFFplay() + if err == nil { + _ = ffplay.Play(file) + } + + fyne.Do(func() { + p.buttonPlay.Enable() + }) + }() + } + }) +} + +type Progress struct { + totalDuration float64 + progressbar *widget.ProgressBar + protocol string +} + +func NewProgress(totalDuration float64, progressbar *widget.ProgressBar) ffmpeg.ProgressContract { + return &Progress{ + totalDuration: totalDuration, + progressbar: progressbar, + protocol: "pipe:", + } +} + +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 = lang.L("errorConverter") + } + return errors.New(errorText) + } + + return nil +} diff --git a/main.go b/main.go index 9250ddb..4455f47 100644 --- a/main.go +++ b/main.go @@ -20,9 +20,9 @@ func main() { } app.SetMetadata(appMetadata) fyneApp := app.New() - progressBarService := convertor.NewProgressBar() appSetting := setting.NewSetting(fyneApp) ffmpegService := ffmpeg.NewUtilities(appSetting) + progressBarService := convertor.NewProgressBar(ffmpegService) convertorService := convertor.NewConvertor(ffmpegService) itemsToConvert := convertor.NewItemsToConvert(ffmpegService) queue := convertor.NewQueueList() @@ -37,5 +37,9 @@ func main() { ) mainController := controller.NewController(myApp) mainController.Start() + + myApp.RunConvertor() + defer myApp.AfterClosing() + myApp.Run() } -- 2.47.2 From 568d8f0897301ba5e2caa845415f68ed87425d71 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sun, 8 Jun 2025 22:28:11 +0500 Subject: [PATCH 18/41] Removed duplicate isChecked, since there is IsChecked. --- internal/gui/window/queue.go | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/internal/gui/window/queue.go b/internal/gui/window/queue.go index 9bcb4c0..f83715a 100644 --- a/internal/gui/window/queue.go +++ b/internal/gui/window/queue.go @@ -322,33 +322,16 @@ func (s *queueAllStatistics) IsChecked(status convertor.StatusContract) bool { func (s *queueAllStatistics) redrawingQueueItems(queueItems *map[int]queueLayoutItem) { for _, item := range *queueItems { - if s.isChecked(item.status) == true && item.CanvasObject.Visible() == false { + if s.IsChecked(item.status) == true && item.CanvasObject.Visible() == false { item.CanvasObject.Show() continue } - if s.isChecked(item.status) == false && item.CanvasObject.Visible() == true { + if s.IsChecked(item.status) == false && item.CanvasObject.Visible() == true { item.CanvasObject.Hide() } } } -func (s *queueAllStatistics) isChecked(status convertor.StatusContract) bool { - if status == convertor.StatusType(convertor.InProgress) { - return s.inProgress.GetCheckbox().Checked - } - if status == convertor.StatusType(convertor.Completed) { - return s.completed.GetCheckbox().Checked - } - if status == convertor.StatusType(convertor.Error) { - return s.error.GetCheckbox().Checked - } - if status == convertor.StatusType(convertor.Waiting) { - return s.waiting.GetCheckbox().Checked - } - - return true -} - func (s *queueAllStatistics) checkboxChecked() { if s.total.GetCheckbox().Checked == true { return -- 2.47.2 From 690f84e2c802c851e57106538508588b155af437 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sun, 8 Jun 2025 22:54:50 +0500 Subject: [PATCH 19/41] Fix display of queues during conversion Fixed the error of displaying queues when they want to see only a certain status. --- internal/gui/window/queue.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/internal/gui/window/queue.go b/internal/gui/window/queue.go index f83715a..c801c1e 100644 --- a/internal/gui/window/queue.go +++ b/internal/gui/window/queue.go @@ -83,7 +83,7 @@ func (l *queueLayout) AddQueue(queueID int, queue *convertor.Queue) { BlockMessageError: blockMessageError, MessageError: messageError, buttonPlay: buttonPlay, - status: queue.Status, + status: &queue.Status, } l.itemsContainer.Add(content) } @@ -121,7 +121,9 @@ func (l *queueLayout) ChangeQueue(queueID int, queue *convertor.Queue) { } if l.GetQueueStatistics().IsChecked(queue.Status) == false && item.CanvasObject.Visible() == true { item.CanvasObject.Hide() - } else if item.CanvasObject.Visible() == false { + } + + if l.GetQueueStatistics().IsChecked(queue.Status) == true && item.CanvasObject.Visible() == false { item.CanvasObject.Show() } @@ -322,11 +324,11 @@ func (s *queueAllStatistics) IsChecked(status convertor.StatusContract) bool { func (s *queueAllStatistics) redrawingQueueItems(queueItems *map[int]queueLayoutItem) { for _, item := range *queueItems { - if s.IsChecked(item.status) == true && item.CanvasObject.Visible() == false { + if s.IsChecked(*item.status) == true && item.CanvasObject.Visible() == false { item.CanvasObject.Show() continue } - if s.IsChecked(item.status) == false && item.CanvasObject.Visible() == true { + if s.IsChecked(*item.status) == false && item.CanvasObject.Visible() == true { item.CanvasObject.Hide() } } @@ -453,5 +455,5 @@ type queueLayoutItem struct { StatusMessage *canvas.Text MessageError *canvas.Text buttonPlay *widget.Button - status convertor.StatusContract + status *convertor.StatusContract } -- 2.47.2 From fc4e585620a6809b52494143e4f4e45c3e909b28 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Mon, 9 Jun 2025 00:27:40 +0500 Subject: [PATCH 20/41] Add main settings view and theme management functionality Introduce a new `MainSettings` view for managing application settings, including language and theme selection. Implement theme management methods in the `setting` package to handle theme initialization, retrieval, and updates. --- internal/application/setting/setting.go | 5 ++ internal/application/setting/theme.go | 110 ++++++++++++++++++++++++ internal/controller/menu.go | 36 ++++++++ internal/gui/view/mainSettings.go | 92 ++++++++++++++++++++ 4 files changed, 243 insertions(+) create mode 100644 internal/application/setting/theme.go create mode 100644 internal/controller/menu.go create mode 100644 internal/gui/view/mainSettings.go diff --git a/internal/application/setting/setting.go b/internal/application/setting/setting.go index f969edc..b6afe5b 100644 --- a/internal/application/setting/setting.go +++ b/internal/application/setting/setting.go @@ -22,6 +22,11 @@ type SettingContract interface { GetFFplayPath() string SetFFplayPath(path string) + + ThemeInit() + GetThemes() map[string]ThemeInfoContract + GetTheme() ThemeInfoContract + SetTheme(themeInfo ThemeInfoContract) } type setting struct { diff --git a/internal/application/setting/theme.go b/internal/application/setting/theme.go new file mode 100644 index 0000000..b81547b --- /dev/null +++ b/internal/application/setting/theme.go @@ -0,0 +1,110 @@ +package setting + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/lang" + "fyne.io/fyne/v2/theme" + "image/color" +) + +func (s *setting) GetTheme() ThemeInfoContract { + name := s.fyneApp.Preferences().String("theme") + if name != "" { + if _, ok := s.GetThemes()[name]; ok { + return s.GetThemes()[name] + } + } + + return s.GetThemes()["default"] +} + +func (s *setting) SetTheme(themeInfo ThemeInfoContract) { + s.fyneApp.Preferences().SetString("theme", themeInfo.GetName()) + + if themeInfo.GetName() == "default" { + s.fyneApp.Settings().SetTheme(theme.DefaultTheme()) + return + } + s.fyneApp.Settings().SetTheme(&forcedVariant{theme: theme.DefaultTheme(), variant: themeInfo.GetVariant()}) +} + +func (s *setting) ThemeInit() { + themeInfo := s.GetTheme() + if themeInfo.GetName() == "default" { + s.fyneApp.Settings().SetTheme(theme.DefaultTheme()) + return + } + s.fyneApp.Settings().SetTheme(&forcedVariant{theme: theme.DefaultTheme(), variant: themeInfo.GetVariant()}) +} + +func (s *setting) GetThemes() map[string]ThemeInfoContract { + themesNameDefault := &themeInfo{ + name: "default", + title: lang.L("themesNameDefault"), + } + + themesNameLight := &themeInfo{ + name: "light", + title: lang.L("themesNameLight"), + variant: theme.VariantLight, + } + + themesNameDark := &themeInfo{ + name: "dark", + title: lang.L("themesNameDark"), + variant: theme.VariantDark, + } + + list := map[string]ThemeInfoContract{ + "default": themesNameDefault, + "light": themesNameLight, + "dark": themesNameDark, + } + + return list +} + +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 +} + +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 theme.DefaultTheme().Font(style) +} + +func (f *forcedVariant) Icon(name fyne.ThemeIconName) fyne.Resource { + return theme.DefaultTheme().Icon(name) +} + +func (f *forcedVariant) Size(name fyne.ThemeSizeName) float32 { + return theme.DefaultTheme().Size(name) +} diff --git a/internal/controller/menu.go b/internal/controller/menu.go new file mode 100644 index 0000000..ec6dab3 --- /dev/null +++ b/internal/controller/menu.go @@ -0,0 +1,36 @@ +package controller + +import ( + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/gui/view" +) + +func (c *controller) actionSettingConvertor() { + c.settingConvertor(true) +} + +func (c *controller) actionMainSettings() { + currentLang, _ := c.app.GetSetting().GetCurrentLangOrDefaultLang() + content := view.MainSettings( + currentLang, + c.app.GetSetting().GetLanguages(), + + c.app.GetSetting().GetTheme(), + c.app.GetSetting().GetThemes(), + + c.actionMainSettingsSave, + c.convertor, + ) + c.window.SetContent(content) +} + +func (c *controller) actionMainSettingsSave(setting *view.MainSettingForm) error { + err := c.app.GetSetting().SetLang(setting.Language) + if err != nil { + return err + } + c.app.GetSetting().SetTheme(setting.ThemeInfo) + c.initLayout() + + c.convertor() + return nil +} diff --git a/internal/gui/view/mainSettings.go b/internal/gui/view/mainSettings.go new file mode 100644 index 0000000..b9e19e7 --- /dev/null +++ b/internal/gui/view/mainSettings.go @@ -0,0 +1,92 @@ +package view + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/lang" + "fyne.io/fyne/v2/widget" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application/setting" + "image/color" +) + +type MainSettingForm struct { + Language setting.Lang + ThemeInfo setting.ThemeInfoContract +} + +func MainSettings( + currentLang setting.Lang, + langList []setting.Lang, + + themeInfo setting.ThemeInfoContract, + themeList map[string]setting.ThemeInfoContract, + + save func(form *MainSettingForm) error, + cancel func(), +) fyne.CanvasObject { + + errorMessage := canvas.NewText("", color.RGBA{R: 255, G: 0, B: 0, A: 255}) + errorMessage.TextSize = 16 + errorMessage.TextStyle = fyne.TextStyle{Bold: true} + + viewSettingForm := &MainSettingForm{ + Language: currentLang, + ThemeInfo: themeInfo, + } + + var languageItems []string + langByTitle := map[string]setting.Lang{} + for _, language := range langList { + 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 = currentLang.Title + + var themeItems []string + themeByTitle := map[string]setting.ThemeInfoContract{} + for _, themeInfo := range themeList { + 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 = themeInfo.GetTitle() + + form := &widget.Form{ + Items: []*widget.FormItem{ + { + Text: lang.L("menuSettingsLanguage"), + Widget: selectLanguage, + }, + { + Text: lang.L("menuSettingsTheme"), + Widget: selectTheme, + }, + { + Widget: errorMessage, + }, + }, + SubmitText: lang.L("save"), + OnSubmit: func() { + err := save(viewSettingForm) + if err != nil { + errorMessage.Text = err.Error() + } + }, + } + if cancel != nil { + form.OnCancel = cancel + form.CancelText = lang.L("cancel") + } + + messageHead := lang.L("settings") + return widget.NewCard(messageHead, "", form) +} -- 2.47.2 From cae996a141a64e50f3c38790939875f0c2fdfdf5 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Mon, 9 Jun 2025 22:54:27 +0500 Subject: [PATCH 21/41] Moved the display of FFplay key descriptions to a new structure. --- internal/gui/view/help_ffplay.go | 98 ++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 internal/gui/view/help_ffplay.go diff --git a/internal/gui/view/help_ffplay.go b/internal/gui/view/help_ffplay.go new file mode 100644 index 0000000..d1848fd --- /dev/null +++ b/internal/gui/view/help_ffplay.go @@ -0,0 +1,98 @@ +package view + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/lang" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +func HelpFFplay() fyne.CanvasObject { + data := [][]string{ + []string{ + lang.L("helpFFplayKeys"), + lang.L("helpFFplayDescription"), + }, + []string{ + "Q, ESC", + lang.L("helpFFplayQuit"), + }, + []string{ + "F, " + lang.L("helpFFplayDoubleClickLeftMouseButton"), + lang.L("helpFFplayToggleFullScreen"), + }, + []string{ + "P, " + lang.L("helpFFplayKeySpace"), + lang.L("helpFFplayPause"), + }, + []string{ + "M", + lang.L("helpFFplayToggleMute"), + }, + []string{ + "9, /", + lang.L("helpFFplayDecreaseVolume"), + }, + []string{ + "0, *", + lang.L("helpFFplayIncreaseVolume"), + }, + []string{ + lang.L("helpFFplayKeyLeft"), + lang.L("helpFFplaySeekBackward10Seconds"), + }, + []string{ + lang.L("helpFFplayKeyRight"), + lang.L("helpFFplaySeekForward10Seconds"), + }, + []string{ + lang.L("helpFFplayKeyDown"), + lang.L("helpFFplaySeekBackward1Minute"), + }, + []string{ + lang.L("helpFFplayKeyUp"), + lang.L("helpFFplaySeekBForward1Minute"), + }, + []string{ + "Page Down", + lang.L("helpFFplaySeekBackward10Minutes"), + }, + []string{ + "Page Up", + lang.L("helpFFplaySeekBForward10Minutes"), + }, + []string{ + "S, " + lang.L("helpFFplayKeyHoldS"), + lang.L("helpFFplayActivateFrameStepMode"), + }, + []string{ + "W", + lang.L("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) + + return container.NewScroll(list) +} -- 2.47.2 From 39080cac14596e85cf17531216b85a0d6105de64 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Mon, 9 Jun 2025 22:56:04 +0500 Subject: [PATCH 22/41] Refactor `HelpFFplay` to simplify array initialization. --- internal/gui/view/help_ffplay.go | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/internal/gui/view/help_ffplay.go b/internal/gui/view/help_ffplay.go index d1848fd..07fbcd0 100644 --- a/internal/gui/view/help_ffplay.go +++ b/internal/gui/view/help_ffplay.go @@ -10,63 +10,63 @@ import ( func HelpFFplay() fyne.CanvasObject { data := [][]string{ - []string{ + { lang.L("helpFFplayKeys"), lang.L("helpFFplayDescription"), }, - []string{ + { "Q, ESC", lang.L("helpFFplayQuit"), }, - []string{ + { "F, " + lang.L("helpFFplayDoubleClickLeftMouseButton"), lang.L("helpFFplayToggleFullScreen"), }, - []string{ + { "P, " + lang.L("helpFFplayKeySpace"), lang.L("helpFFplayPause"), }, - []string{ + { "M", lang.L("helpFFplayToggleMute"), }, - []string{ + { "9, /", lang.L("helpFFplayDecreaseVolume"), }, - []string{ + { "0, *", lang.L("helpFFplayIncreaseVolume"), }, - []string{ + { lang.L("helpFFplayKeyLeft"), lang.L("helpFFplaySeekBackward10Seconds"), }, - []string{ + { lang.L("helpFFplayKeyRight"), lang.L("helpFFplaySeekForward10Seconds"), }, - []string{ + { lang.L("helpFFplayKeyDown"), lang.L("helpFFplaySeekBackward1Minute"), }, - []string{ + { lang.L("helpFFplayKeyUp"), lang.L("helpFFplaySeekBForward1Minute"), }, - []string{ + { "Page Down", lang.L("helpFFplaySeekBackward10Minutes"), }, - []string{ + { "Page Up", lang.L("helpFFplaySeekBForward10Minutes"), }, - []string{ + { "S, " + lang.L("helpFFplayKeyHoldS"), lang.L("helpFFplayActivateFrameStepMode"), }, - []string{ + { "W", lang.L("helpFFplayCycleVideoFiltersOrShowModes"), }, -- 2.47.2 From c49957e58305cb60ebce3217dd2c34ced68c6f79 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Mon, 9 Jun 2025 23:12:20 +0500 Subject: [PATCH 23/41] Moved the "About" window to a new structure. --- internal/gui/view/about.go | 620 +++++++++++++++++++++++++++++++++++++ 1 file changed, 620 insertions(+) create mode 100644 internal/gui/view/about.go diff --git a/internal/gui/view/about.go b/internal/gui/view/about.go new file mode 100644 index 0000000..5744bad --- /dev/null +++ b/internal/gui/view/about.go @@ -0,0 +1,620 @@ +package view + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/lang" + "fyne.io/fyne/v2/widget" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/resources" + "golang.org/x/image/colornames" + "net/url" +) + +func About(appVersion string, ffmpegVersion string, ffprobeVersion string, ffplayVersion string) fyne.CanvasObject { + programmName := canvas.NewText(" GUI for FFmpeg", colornames.Darkgreen) + programmName.TextStyle = fyne.TextStyle{Bold: true} + programmName.TextSize = 20 + + programmLink := widget.NewHyperlink( + lang.L("programmLink"), + &url.URL{ + Scheme: "https", + Host: "gui-for-ffmpeg.projects.kor-elf.net", + Path: "/", + }, + ) + + licenseLink := widget.NewHyperlink( + lang.L("licenseLink"), + &url.URL{ + Scheme: "https", + Host: "git.kor-elf.net", + Path: "kor-elf/gui-for-ffmpeg/src/branch/main/LICENSE", + }, + ) + + licenseLinkOther := widget.NewHyperlink( + lang.L("licenseLinkOther"), + &url.URL{ + Scheme: "https", + Host: "git.kor-elf.net", + Path: "kor-elf/gui-for-ffmpeg/src/branch/main/LICENSE-3RD-PARTY.txt", + }, + ) + + programmVersion := widget.NewRichTextFromMarkdown( + lang.L( + "programmVersion", + map[string]any{"Version": appVersion}, + ), + ) + + aboutText := widget.NewRichText( + &widget.TextSegment{ + Text: lang.L("aboutText"), + }, + ) + image := canvas.NewImageFromResource(resources.IconAppLogoResource()) + image.SetMinSize(fyne.Size{Width: 100, Height: 100}) + image.FillMode = canvas.ImageFillContain + + ffmpegTrademark := widget.NewRichTextFromMarkdown(lang.L("ffmpegTrademark")) + ffmpegLGPL := widget.NewRichTextFromMarkdown(lang.L("ffmpegLGPL")) + + return container.NewScroll(container.NewVBox( + container.NewBorder(nil, nil, container.NewVBox(image), nil, container.NewVBox( + programmName, + programmVersion, + aboutText, + ffmpegTrademark, + ffmpegLGPL, + widget.NewRichTextFromMarkdown("Copyright (c) 2024 **[Leonid Nikitin (kor-elf)](https://git.kor-elf.net/kor-elf/)**."), + container.NewHBox(programmLink, licenseLink), + container.NewHBox(licenseLinkOther), + )), + aboutFFmpeg(ffmpegVersion), + aboutFFprobe(ffprobeVersion), + aboutFFplay(ffplayVersion), + widget.NewCard(lang.L("AlsoUsedProgram"), "", license3RDParty()), + )) +} + +func aboutFFmpeg(version string) *fyne.Container { + programmName := canvas.NewText(" FFmpeg", colornames.Darkgreen) + programmName.TextStyle = fyne.TextStyle{Bold: true} + programmName.TextSize = 20 + + programmLink := widget.NewHyperlink(lang.L("programmLink"), &url.URL{ + Scheme: "https", + Host: "ffmpeg.org", + Path: "", + }) + + licenseLink := widget.NewHyperlink(lang.L("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](https://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](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html)**."), + container.NewHBox(programmLink, licenseLink), + ) +} + +func aboutFFprobe(version string) *fyne.Container { + programmName := canvas.NewText(" FFprobe", colornames.Darkgreen) + programmName.TextStyle = fyne.TextStyle{Bold: true} + programmName.TextSize = 20 + + programmLink := widget.NewHyperlink(lang.L("programmLink"), &url.URL{ + Scheme: "https", + Host: "ffmpeg.org", + Path: "ffprobe.html", + }) + + licenseLink := widget.NewHyperlink(lang.L("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](https://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](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html)**."), + container.NewHBox(programmLink, licenseLink), + ) +} + +func aboutFFplay(version string) *fyne.Container { + programmName := canvas.NewText(" FFplay", colornames.Darkgreen) + programmName.TextStyle = fyne.TextStyle{Bold: true} + programmName.TextSize = 20 + + programmLink := widget.NewHyperlink(lang.L("programmLink"), &url.URL{ + Scheme: "https", + Host: "ffmpeg.org", + Path: "ffplay.html", + }) + + licenseLink := widget.NewHyperlink(lang.L("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](https://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](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html)**."), + container.NewHBox(programmLink, licenseLink), + ) +} + +func license3RDParty() *fyne.Container { + return container.NewVBox( + canvas.NewLine(colornames.Darkgreen), + + container.NewHBox(widget.NewHyperlink("fyne.io/fyne/v2", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "fyne-io/fyne", + })), + container.NewHBox(widget.NewHyperlink("BSD 3-Clause License", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "fyne-io/fyne/blob/master/LICENSE", + })), + widget.NewRichTextFromMarkdown("Copyright (C) 2018 Fyne.io developers (see [AUTHORS](https://github.com/fyne-io/fyne/blob/master/AUTHORS))"), + canvas.NewLine(colornames.Darkgreen), + + container.NewHBox(widget.NewHyperlink("fyne.io/systray", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "fyne-io/systray", + })), + container.NewHBox(widget.NewHyperlink("Apache License", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "fyne-io/systray/blob/master/LICENSE", + })), + canvas.NewLine(colornames.Darkgreen), + + container.NewHBox(widget.NewHyperlink("github.com/BurntSushi/toml", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "BurntSushi/toml", + })), + container.NewHBox(widget.NewHyperlink("The MIT License (MIT)", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "BurntSushi/toml/blob/master/COPYING", + })), + widget.NewLabel("Copyright (c) 2013 TOML authors"), + canvas.NewLine(colornames.Darkgreen), + + container.NewHBox(widget.NewHyperlink("github.com/davecgh/go-spew", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "davecgh/go-spew", + })), + container.NewHBox(widget.NewHyperlink("ISC License", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "davecgh/go-spew/blob/master/LICENSE", + })), + widget.NewLabel("Copyright (c) 2012-2016 Dave Collins "), + canvas.NewLine(colornames.Darkgreen), + + container.NewHBox(widget.NewHyperlink("github.com/fredbi/uri", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "fredbi/uri", + })), + container.NewHBox(widget.NewHyperlink("The MIT License (MIT)", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "fredbi/uri/blob/master/LICENSE.md", + })), + widget.NewLabel("Copyright (c) 2018 Frederic Bidon"), + widget.NewLabel("Copyright (c) 2015 Trey Tacon"), + canvas.NewLine(colornames.Darkgreen), + + container.NewHBox(widget.NewHyperlink("github.com/fsnotify/fsnotify", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "fsnotify/fsnotify", + })), + container.NewHBox(widget.NewHyperlink("BSD-3-Clause license", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "fsnotify/fsnotify/blob/main/LICENSE", + })), + widget.NewLabel("Copyright © 2012 The Go Authors. All rights reserved."), + widget.NewLabel("Copyright © fsnotify Authors. All rights reserved."), + canvas.NewLine(colornames.Darkgreen), + + container.NewHBox(widget.NewHyperlink("github.com/fyne-io/gl-js", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "fyne-io/gl-js", + })), + container.NewHBox(widget.NewHyperlink("BSD-3-Clause license", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "fyne-io/gl-js/blob/master/LICENSE", + })), + widget.NewLabel("Copyright (c) 2009 The Go Authors. All rights reserved."), + canvas.NewLine(colornames.Darkgreen), + + container.NewHBox(widget.NewHyperlink("github.com/fyne-io/glfw-js", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "fyne-io/glfw-js", + })), + container.NewHBox(widget.NewHyperlink("MIT License", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "fyne-io/glfw-js/blob/master/LICENSE", + })), + widget.NewLabel("Copyright (c) 2014 Dmitri Shuralyov"), + canvas.NewLine(colornames.Darkgreen), + + container.NewHBox(widget.NewHyperlink("github.com/fyne-io/image", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "fyne-io/image", + })), + container.NewHBox(widget.NewHyperlink("BSD 3-Clause License", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "fyne-io/image/blob/main/LICENSE", + })), + widget.NewLabel("Copyright (c) 2022, Fyne.io"), + canvas.NewLine(colornames.Darkgreen), + + container.NewHBox(widget.NewHyperlink("github.com/fyne-io/oksvg", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "fyne-io/oksvg", + })), + container.NewHBox(widget.NewHyperlink("BSD 3-Clause License", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "fyne-io/oksvg/blob/master/LICENSE", + })), + widget.NewLabel("Copyright (c) 2018, Steven R Wiley. All rights reserved."), + canvas.NewLine(colornames.Darkgreen), + + container.NewHBox(widget.NewHyperlink("github.com/go-gl/gl", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "go-gl/gl", + })), + container.NewHBox(widget.NewHyperlink("The MIT License (MIT)", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "go-gl/gl/blob/master/LICENSE", + })), + widget.NewLabel("Copyright (c) 2014 Eric Woroshow"), + canvas.NewLine(colornames.Darkgreen), + + container.NewHBox(widget.NewHyperlink("github.com/go-gl/glfw/v3.3/glfw", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "go-gl/glfw/", + })), + container.NewHBox(widget.NewHyperlink("BSD-3-Clause license", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "go-gl/glfw/blob/master/LICENSE", + })), + widget.NewLabel("Copyright (c) 2012 The glfw3-go Authors. All rights reserved."), + canvas.NewLine(colornames.Darkgreen), + + container.NewHBox(widget.NewHyperlink("github.com/go-text/render", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "go-text/render", + })), + container.NewHBox(widget.NewHyperlink("Unlicense OR BSD-3-Clause", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "go-text/render/blob/main/LICENSE", + })), + widget.NewLabel("Copyright 2021 The go-text authors"), + canvas.NewLine(colornames.Darkgreen), + + container.NewHBox(widget.NewHyperlink("github.com/go-text/typesetting", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "go-text/typesetting", + })), + container.NewHBox(widget.NewHyperlink("Unlicense OR BSD-3-Clause", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "go-text/typesetting/blob/main/LICENSE", + })), + widget.NewLabel("Copyright 2021 The go-text authors"), + canvas.NewLine(colornames.Darkgreen), + + container.NewHBox(widget.NewHyperlink("github.com/godbus/dbus/v5", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "godbus/dbus", + })), + container.NewHBox(widget.NewHyperlink("BSD 2-Clause \"Simplified\" License", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "godbus/dbus/blob/master/LICENSE", + })), + widget.NewLabel("Copyright (c) 2013, Georg Reinke (), Google. All rights reserved."), + canvas.NewLine(colornames.Darkgreen), + + container.NewHBox(widget.NewHyperlink("github.com/hack-pad/go-indexeddb", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "hack-pad/go-indexeddb", + })), + container.NewHBox(widget.NewHyperlink("Apache License 2.0", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "hack-pad/go-indexeddb/blob/main/LICENSE", + })), + canvas.NewLine(colornames.Darkgreen), + + container.NewHBox(widget.NewHyperlink("github.com/hack-pad/safejs", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "hack-pad/safejs", + })), + container.NewHBox(widget.NewHyperlink("Apache License 2.0", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "hack-pad/safejs/blob/main/LICENSE", + })), + canvas.NewLine(colornames.Darkgreen), + + container.NewHBox(widget.NewHyperlink("github.com/jeandeaual/go-locale", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "jeandeaual/go-locale", + })), + container.NewHBox(widget.NewHyperlink("MIT License", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "jeandeaual/go-locale/blob/master/LICENSE", + })), + widget.NewLabel("Copyright (c) 2020 Alexis Jeandeau"), + canvas.NewLine(colornames.Darkgreen), + + container.NewHBox(widget.NewHyperlink("github.com/jsummers/gobmp", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "jsummers/gobmp", + })), + container.NewHBox(widget.NewHyperlink("The MIT License (MIT)", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "jsummers/gobmp/blob/master/COPYING.txt", + })), + widget.NewLabel("Copyright (c) 2012-2015 Jason Summers"), + canvas.NewLine(colornames.Darkgreen), + + container.NewHBox(widget.NewHyperlink("github.com/nfnt/resize", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "nfnt/resize", + })), + container.NewHBox(widget.NewHyperlink("ISC License", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "nfnt/resize/blob/master/LICENSE", + })), + widget.NewLabel("Copyright (c) 2012, Jan Schlicht "), + canvas.NewLine(colornames.Darkgreen), + + container.NewHBox(widget.NewHyperlink("github.com/nicksnyder/go-i18n/v2", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "nicksnyder/go-i18n", + })), + container.NewHBox(widget.NewHyperlink("The MIT License (MIT)", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "nicksnyder/go-i18n/blob/main/LICENSE", + })), + widget.NewRichTextFromMarkdown("Copyright (c) 2014 Nick Snyder [https://github.com/nicksnyder](https://github.com/nicksnyder)"), + canvas.NewLine(colornames.Darkgreen), + + container.NewHBox(widget.NewHyperlink("github.com/pmezard/go-difflib", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "pmezard/go-difflib", + })), + container.NewHBox(widget.NewHyperlink("License", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "pmezard/go-difflib/blob/master/LICENSE", + })), + widget.NewLabel("Copyright (c) 2013, Patrick Mezard. All rights reserved."), + canvas.NewLine(colornames.Darkgreen), + + container.NewHBox(widget.NewHyperlink("github.com/rymdport/portal", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "rymdport/portal", + })), + container.NewHBox(widget.NewHyperlink("Apache License 2.0", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "rymdport/portal/blob/main/LICENSE", + })), + canvas.NewLine(colornames.Darkgreen), + + container.NewHBox(widget.NewHyperlink("github.com/srwiley/oksvg", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "srwiley/oksvg", + })), + container.NewHBox(widget.NewHyperlink("BSD 3-Clause License", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "srwiley/oksvg/blob/master/LICENSE", + })), + widget.NewLabel("Copyright (c) 2018, Steven R Wiley. All rights reserved."), + canvas.NewLine(colornames.Darkgreen), + + container.NewHBox(widget.NewHyperlink("github.com/srwiley/rasterx", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "srwiley/rasterx", + })), + container.NewHBox(widget.NewHyperlink("BSD 3-Clause License", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "srwiley/rasterx/blob/master/LICENSE", + })), + widget.NewLabel("Copyright (c) 2018, Steven R Wiley. All rights reserved."), + canvas.NewLine(colornames.Darkgreen), + + container.NewHBox(widget.NewHyperlink("github.com/stretchr/testify", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "stretchr/testify", + })), + container.NewHBox(widget.NewHyperlink("MIT License", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "stretchr/testify/blob/master/LICENSE", + })), + widget.NewLabel("Copyright (c) 2012-2020 Mat Ryer, Tyler Bunnell and contributors."), + canvas.NewLine(colornames.Darkgreen), + + container.NewHBox(widget.NewHyperlink("github.com/ulikunitz/xz", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "ulikunitz/xz", + })), + container.NewHBox(widget.NewHyperlink("License", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "ulikunitz/xz/blob/master/LICENSE", + })), + widget.NewLabel("Copyright (c) 2014-2022 Ulrich Kunitz. All rights reserved."), + canvas.NewLine(colornames.Darkgreen), + + container.NewHBox(widget.NewHyperlink("github.com/yuin/goldmark", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "yuin/goldmark", + })), + container.NewHBox(widget.NewHyperlink("MIT License", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "yuin/goldmark/blob/master/LICENSE", + })), + widget.NewLabel("Copyright (c) 2019 Yusuke Inuzuka"), + canvas.NewLine(colornames.Darkgreen), + + container.NewHBox(widget.NewHyperlink("go.etcd.io/bbolt", &url.URL{ + Scheme: "https", + Host: "pkg.go.dev", + Path: "go.etcd.io/bbolt", + })), + container.NewHBox(widget.NewHyperlink("MIT License", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "etcd-io/bbolt/blob/main/LICENSE", + })), + widget.NewLabel("Copyright (c) 2013 Ben Johnson"), + canvas.NewLine(colornames.Darkgreen), + + container.NewHBox(widget.NewHyperlink("golang.org/x/image", &url.URL{ + Scheme: "https", + Host: "pkg.go.dev", + Path: "golang.org/x/image", + })), + container.NewHBox(widget.NewHyperlink("License", &url.URL{ + Scheme: "https", + Host: "cs.opensource.google", + Path: "go/x/image/+/master:LICENSE", + })), + widget.NewLabel("Copyright 2009 The Go Authors."), + canvas.NewLine(colornames.Darkgreen), + + container.NewHBox(widget.NewHyperlink("golang.org/x/net", &url.URL{ + Scheme: "https", + Host: "pkg.go.dev", + Path: "golang.org/x/net", + })), + container.NewHBox(widget.NewHyperlink("License", &url.URL{ + Scheme: "https", + Host: "cs.opensource.google", + Path: "go/x/net/+/master:LICENSE", + })), + widget.NewLabel("Copyright 2009 The Go Authors."), + canvas.NewLine(colornames.Darkgreen), + + container.NewHBox(widget.NewHyperlink("golang.org/x/sys", &url.URL{ + Scheme: "https", + Host: "pkg.go.dev", + Path: "golang.org/x/sys", + })), + container.NewHBox(widget.NewHyperlink("License", &url.URL{ + Scheme: "https", + Host: "cs.opensource.google", + Path: "go/x/sys/+/master:LICENSE", + })), + widget.NewLabel("Copyright 2009 The Go Authors."), + canvas.NewLine(colornames.Darkgreen), + + container.NewHBox(widget.NewHyperlink("golang.org/x/text", &url.URL{ + Scheme: "https", + Host: "pkg.go.dev", + Path: "golang.org/x/text", + })), + container.NewHBox(widget.NewHyperlink("License", &url.URL{ + Scheme: "https", + Host: "cs.opensource.google", + Path: "go/x/text/+/master:LICENSE", + })), + widget.NewLabel("Copyright 2009 The Go Authors."), + canvas.NewLine(colornames.Darkgreen), + + container.NewHBox(widget.NewHyperlink("gopkg.in/yaml.v3", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "go-yaml/yaml/tree/v3.0.1", + })), + container.NewHBox(widget.NewHyperlink("MIT License and Apache License 2.0", &url.URL{ + Scheme: "http", + Host: "github.com", + Path: "go-yaml/yaml/blob/v3.0.1/LICENSE", + })), + widget.NewLabel("Copyright (c) 2006-2010 Kirill Simonov"), + widget.NewLabel("Copyright (c) 2006-2011 Kirill Simonov"), + widget.NewLabel("Copyright (c) 2011-2019 Canonical Ltd"), + canvas.NewLine(colornames.Darkgreen), + + container.NewHBox(widget.NewHyperlink("github.com/golang/go", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "golang/go", + })), + container.NewHBox(widget.NewHyperlink("BSD 3-Clause \"New\" or \"Revised\" License", &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "golang/go/blob/master/LICENSE", + })), + widget.NewLabel("Copyright 2009 The Go Authors."), + canvas.NewLine(colornames.Darkgreen), + ) +} -- 2.47.2 From c8619cdc7f9d5e93f8b0389a8affb521e20ac11f Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Mon, 9 Jun 2025 23:19:48 +0500 Subject: [PATCH 24/41] Added the ability to get the FFmpeg version. --- internal/ffmpeg/ffmpeg.go | 13 +++++++++++++ internal/ffmpeg/utilities.go | 14 ++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index b303182..515e180 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -7,6 +7,7 @@ import ( "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/utils" "io" "os/exec" + "regexp" "strings" ) @@ -17,6 +18,7 @@ type ProgressContract interface { type FFmpegContract interface { GetPath() string + GetFFmpegVersion() (string, error) GetEncoders(scanner func(scanner *bufio.Reader)) error RunConvert(setting ConvertSetting, progress ProgressContract, beforeWait func(cmd *exec.Cmd), afterWait func(cmd *exec.Cmd)) error } @@ -47,6 +49,17 @@ func (f *ffmpeg) GetPath() string { return f.path } +func (f *ffmpeg) GetFFmpegVersion() (string, error) { + cmd := exec.Command(f.path, "-version") + utils.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 (f *ffmpeg) RunConvert(setting ConvertSetting, progress ProgressContract, beforeWait func(cmd *exec.Cmd), afterWait func(cmd *exec.Cmd)) error { overwriteOutputFiles := "-n" if setting.OverwriteOutputFiles == true { diff --git a/internal/ffmpeg/utilities.go b/internal/ffmpeg/utilities.go index 214f862..61b680b 100644 --- a/internal/ffmpeg/utilities.go +++ b/internal/ffmpeg/utilities.go @@ -24,6 +24,7 @@ type UtilitiesContract interface { UtilityCheck() bool GetFFmpeg() (FFmpegContract, error) + GetFFmpegVersion() string GetFFmpegPath() string ChangeFFmpeg(path string) error @@ -82,6 +83,19 @@ func (u *utilities) GetFFmpeg() (FFmpegContract, error) { return u.ffmpeg, nil } +func (u *utilities) GetFFmpegVersion() string { + ffmpegService, err := u.GetFFmpeg() + if err != nil { + return lang.L("errorFFmpegVersion") + } + + version, err := ffmpegService.GetFFmpegVersion() + if err != nil { + return lang.L("errorFFmpegVersion") + } + return version +} + func (u *utilities) GetFFmpegPath() string { ffmpegService, err := u.GetFFmpeg() if err != nil { -- 2.47.2 From a9c59137af15bc5135a07ef9d3e766dc48a46596 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Mon, 9 Jun 2025 23:21:19 +0500 Subject: [PATCH 25/41] Rename `GetFFmpegVersion` to `GetVersion` in `FFmpegContract` for consistency and clarity. --- internal/ffmpeg/ffmpeg.go | 4 ++-- internal/ffmpeg/utilities.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index 515e180..e4046d1 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -18,7 +18,7 @@ type ProgressContract interface { type FFmpegContract interface { GetPath() string - GetFFmpegVersion() (string, error) + GetVersion() (string, error) GetEncoders(scanner func(scanner *bufio.Reader)) error RunConvert(setting ConvertSetting, progress ProgressContract, beforeWait func(cmd *exec.Cmd), afterWait func(cmd *exec.Cmd)) error } @@ -49,7 +49,7 @@ func (f *ffmpeg) GetPath() string { return f.path } -func (f *ffmpeg) GetFFmpegVersion() (string, error) { +func (f *ffmpeg) GetVersion() (string, error) { cmd := exec.Command(f.path, "-version") utils.PrepareBackgroundCommand(cmd) out, err := cmd.CombinedOutput() diff --git a/internal/ffmpeg/utilities.go b/internal/ffmpeg/utilities.go index 61b680b..b0802b7 100644 --- a/internal/ffmpeg/utilities.go +++ b/internal/ffmpeg/utilities.go @@ -89,7 +89,7 @@ func (u *utilities) GetFFmpegVersion() string { return lang.L("errorFFmpegVersion") } - version, err := ffmpegService.GetFFmpegVersion() + version, err := ffmpegService.GetVersion() if err != nil { return lang.L("errorFFmpegVersion") } -- 2.47.2 From d7428683e4e32b8077dc21917d4a63c5775db13e Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Mon, 9 Jun 2025 23:25:13 +0500 Subject: [PATCH 26/41] Add `GetFFprobeVersion` method to retrieve FFprobe version details Extended `FFprobeContract` with `GetVersion` to fetch FFprobe version. Implemented version extraction in `utilities` and `ffprobe` to support version retrieval functionality. --- internal/ffmpeg/ffprobe.go | 13 +++++++++++++ internal/ffmpeg/utilities.go | 14 ++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/internal/ffmpeg/ffprobe.go b/internal/ffmpeg/ffprobe.go index ac8c2f9..e56299d 100644 --- a/internal/ffmpeg/ffprobe.go +++ b/internal/ffmpeg/ffprobe.go @@ -5,6 +5,7 @@ import ( "fyne.io/fyne/v2/lang" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/utils" "os/exec" + "regexp" "strconv" "strings" "unicode" @@ -12,6 +13,7 @@ import ( type FFprobeContract interface { GetPath() string + GetVersion() (string, error) GetTotalDuration(file *File) (float64, error) } @@ -41,6 +43,17 @@ func (f *ffprobe) GetPath() string { return f.path } +func (f *ffprobe) GetVersion() (string, error) { + cmd := exec.Command(f.path, "-version") + utils.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 (f *ffprobe) GetTotalDuration(file *File) (duration float64, err error) { args := []string{"-v", "error", "-select_streams", "v:0", "-count_packets", "-show_entries", "stream=nb_read_packets", "-of", "csv=p=0", file.Path} cmd := exec.Command(f.path, args...) diff --git a/internal/ffmpeg/utilities.go b/internal/ffmpeg/utilities.go index b0802b7..c6fcfc1 100644 --- a/internal/ffmpeg/utilities.go +++ b/internal/ffmpeg/utilities.go @@ -29,6 +29,7 @@ type UtilitiesContract interface { ChangeFFmpeg(path string) error GetFFprobe() (FFprobeContract, error) + GetFFprobeVersion() string GetFFprobePath() string ChangeFFprobe(path string) error @@ -132,6 +133,19 @@ func (u *utilities) GetFFprobe() (FFprobeContract, error) { return u.ffprobe, nil } +func (u *utilities) GetFFprobeVersion() string { + ffprobeService, err := u.GetFFprobe() + if err != nil { + return lang.L("errorFFprobeVersion") + } + + ffprobeVersion, err := ffprobeService.GetVersion() + if err != nil { + return lang.L("errorFFprobeVersion") + } + return ffprobeVersion +} + func (u *utilities) GetFFprobePath() string { ffprobeService, err := u.GetFFprobe() if err != nil { -- 2.47.2 From e6db5909378aab1822d4aefb348739ec1bf4975c Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Mon, 9 Jun 2025 23:28:25 +0500 Subject: [PATCH 27/41] Add `GetFFplayVersion` method to retrieve FFplay version details Extended `FFplayContract` with `GetVersion` to fetch FFplay version. Implemented version extraction in `utilities` and `ffplay` to support version retrieval functionality. --- internal/ffmpeg/ffplay.go | 13 +++++++++++++ internal/ffmpeg/utilities.go | 14 ++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/internal/ffmpeg/ffplay.go b/internal/ffmpeg/ffplay.go index c888842..bb6454d 100644 --- a/internal/ffmpeg/ffplay.go +++ b/internal/ffmpeg/ffplay.go @@ -5,11 +5,13 @@ import ( "fyne.io/fyne/v2/lang" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/utils" "os/exec" + "regexp" "strings" ) type FFplayContract interface { GetPath() string + GetVersion() (string, error) Play(file *File) error } @@ -39,6 +41,17 @@ func (f *ffplay) GetPath() string { return f.path } +func (f *ffplay) GetVersion() (string, error) { + cmd := exec.Command(f.path, "-version") + utils.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 (f *ffplay) Play(file *File) error { args := []string{file.Path} cmd := exec.Command(f.GetPath(), args...) diff --git a/internal/ffmpeg/utilities.go b/internal/ffmpeg/utilities.go index c6fcfc1..06dae49 100644 --- a/internal/ffmpeg/utilities.go +++ b/internal/ffmpeg/utilities.go @@ -34,6 +34,7 @@ type UtilitiesContract interface { ChangeFFprobe(path string) error GetFFplay() (FFplayContract, error) + GetFFplayVersion() string GetFFplayPath() string ChangeFFplay(path string) error } @@ -182,6 +183,19 @@ func (u *utilities) GetFFplay() (FFplayContract, error) { return u.ffplay, nil } +func (u *utilities) GetFFplayVersion() string { + ffplayService, err := u.GetFFplay() + if err != nil { + return lang.L("errorFFplayVersion") + } + + ffplayVersion, err := ffplayService.GetVersion() + if err != nil { + return lang.L("errorFFplayVersion") + } + return ffplayVersion +} + func (u *utilities) GetFFplayPath() string { ffplayService, err := u.GetFFplay() if err != nil { -- 2.47.2 From 077d7a82a9730cd09e5d8b2e2c55d66f091dbeff Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Mon, 9 Jun 2025 23:30:05 +0500 Subject: [PATCH 28/41] Moved the menu to a new structure. --- internal/controller/main.go | 22 ++++++++++++++++++++-- internal/controller/menu.go | 31 +++++++++++++++++++++++++++++++ internal/gui/menu/main.go | 29 +++++++++++++++++++++++++++++ internal/gui/window/main.go | 7 +++++++ 4 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 internal/gui/menu/main.go diff --git a/internal/controller/main.go b/internal/controller/main.go index 78dff67..9421ace 100644 --- a/internal/controller/main.go +++ b/internal/controller/main.go @@ -1,8 +1,10 @@ package controller import ( + "fyne.io/fyne/v2" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application/setting" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/gui/menu" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/gui/view" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/gui/window" ) @@ -40,6 +42,8 @@ func (c *controller) Start() { return } + c.app.GetSetting().ThemeInit() + if isDefault { languages := c.app.GetSetting().GetLanguages() content := view.StartWithoutSupportLang(languages, func(lang setting.Lang) { @@ -48,7 +52,7 @@ func (c *controller) Start() { c.startWithError(err) return } - c.window.InitLayout() + c.initLayout() c.verificareaFFmpeg() }) c.window.SetContent(content) @@ -56,7 +60,7 @@ func (c *controller) Start() { return } - c.window.InitLayout() + c.initLayout() c.verificareaFFmpeg() c.window.Show() } @@ -75,3 +79,17 @@ func (c *controller) initLanguage() (isDefault bool, err error) { err = setting.ChangeLang(lang) return isDefault, err } + +func (c *controller) initLayout() { + c.window.SetMainMenu(fyne.NewMainMenu( + menu.MainMenuSettings( + c.actionMainSettings, + c.actionSettingConvertor, + ), + menu.MainMenuHelp( + c.actionAbout, + c.actionHelpFFplay, + ), + )) + c.window.InitLayout() +} diff --git a/internal/controller/menu.go b/internal/controller/menu.go index ec6dab3..71e45ca 100644 --- a/internal/controller/menu.go +++ b/internal/controller/menu.go @@ -1,6 +1,8 @@ package controller import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/lang" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/gui/view" ) @@ -34,3 +36,32 @@ func (c *controller) actionMainSettingsSave(setting *view.MainSettingForm) error c.convertor() return nil } + +func (c *controller) actionAbout() { + ffmpegVersion := c.app.GetFFmpegService().GetFFmpegVersion() + ffprobeVersion := c.app.GetFFmpegService().GetFFprobeVersion() + ffplayVersion := c.app.GetFFmpegService().GetFFplayVersion() + appVersion := c.app.FyneApp().Metadata().Version + + window := c.app.FyneApp().NewWindow(lang.L("about")) + window.Resize(fyne.Size{Width: 793, Height: 550}) + window.SetFixedSize(true) + + content := view.About(appVersion, ffmpegVersion, ffprobeVersion, ffplayVersion) + + window.SetContent(content) + window.CenterOnScreen() + window.Show() +} + +func (c *controller) actionHelpFFplay() { + window := c.app.FyneApp().NewWindow(lang.L("helpFFplay")) + window.Resize(fyne.Size{Width: 800, Height: 550}) + window.SetFixedSize(true) + + content := view.HelpFFplay() + + window.SetContent(content) + window.CenterOnScreen() + window.Show() +} diff --git a/internal/gui/menu/main.go b/internal/gui/menu/main.go new file mode 100644 index 0000000..6afe023 --- /dev/null +++ b/internal/gui/menu/main.go @@ -0,0 +1,29 @@ +package menu + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/lang" +) + +func MainMenuSettings( + actionMainSettings func(), + actionFFPathSelection func(), +) *fyne.Menu { + quit := fyne.NewMenuItem(lang.L("exit"), nil) + quit.IsQuit = true + + settingsSelection := fyne.NewMenuItem(lang.L("settings"), actionMainSettings) + ffPathSelection := fyne.NewMenuItem(lang.L("changeFFPath"), actionFFPathSelection) + + return fyne.NewMenu(lang.L("settings"), settingsSelection, ffPathSelection, quit) +} + +func MainMenuHelp( + actionAbout func(), + actionHelpFFplay func(), +) *fyne.Menu { + about := fyne.NewMenuItem(lang.L("about"), actionAbout) + helpFFplay := fyne.NewMenuItem(lang.L("helpFFplay"), actionHelpFFplay) + + return fyne.NewMenu(lang.L("help"), helpFFplay, about) +} diff --git a/internal/gui/window/main.go b/internal/gui/window/main.go index 98ff5f4..4fcb823 100644 --- a/internal/gui/window/main.go +++ b/internal/gui/window/main.go @@ -9,6 +9,7 @@ import ( type WindowContract interface { SetContent(content fyne.CanvasObject) + SetMainMenu(menu *fyne.MainMenu) Show() InitLayout() NewFileOpen(callback func(fyne.URIReadCloser, error), location fyne.ListableURI) *dialog.FileDialog @@ -42,6 +43,12 @@ func NewMainWindow( } } +func (w *mainWindow) SetMainMenu(menu *fyne.MainMenu) { + fyne.Do(func() { + w.fyneWindow.SetMainMenu(menu) + }) +} + func (w *mainWindow) InitLayout() { fyne.Do(func() { w.layout = NewLayout(w.progressBarService, w.itemsToConvert, w.queueLayout) -- 2.47.2 From f4604f94c611c6beae8b4b28b0f6b33313489c89 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Mon, 9 Jun 2025 23:38:50 +0500 Subject: [PATCH 29/41] Set Fyne window as master during controller initialization. --- internal/controller/main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/controller/main.go b/internal/controller/main.go index 9421ace..8d75dc9 100644 --- a/internal/controller/main.go +++ b/internal/controller/main.go @@ -20,6 +20,7 @@ type controller struct { func NewController(app application.AppContract) ControllerContract { fyneWindow := app.FyneApp().NewWindow(app.FyneApp().Metadata().Name) + fyneWindow.SetMaster() queueLayout := window.NewQueueLayout(app.GetFFmpegService()) app.GetQueueService().AddListener(queueLayout) -- 2.47.2 From 850cbbaf7081d122f793ae98398cd2a55d502017 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Mon, 9 Jun 2025 23:42:09 +0500 Subject: [PATCH 30/41] Removed the old program structure. --- convertor/repository.go | 46 -- convertor/view.go | 60 -- convertor/view/conversion.go | 515 ------------ convertor/view/form_items/form.go | 16 - convertor/view/form_items/h264_nvenc/view.go | 64 -- convertor/view/form_items/libx264/view.go | 64 -- convertor/view/form_items/libx265/view.go | 64 -- convertor/view_setting.go | 128 --- ...ew_setting_button_download_ffmpeg_anyos.go | 17 - ...ew_setting_button_download_ffmpeg_linux.go | 61 -- ..._setting_button_download_ffmpeg_windows.go | 61 -- encoder/apng/encoder.go | 19 - encoder/bmp/encoder.go | 19 - encoder/encoder.go | 169 ---- encoder/flv/encoder.go | 19 - encoder/gif/encoder.go | 19 - encoder/h264_nvenc/encoder.go | 58 -- encoder/libmp3lame/encoder.go | 19 - encoder/libshine/encoder.go | 19 - encoder/libtwolame/encoder.go | 19 - encoder/libvpx/encoder.go | 19 - encoder/libvpx_vp9/encoder.go | 19 - encoder/libwebp/encoder.go | 19 - encoder/libwebp_anim/encoder.go | 19 - encoder/libx264/encoder.go | 56 -- encoder/libx265/encoder.go | 56 -- encoder/libxvid/encoder.go | 19 - encoder/mjpeg/encoder.go | 19 - encoder/mp2/encoder.go | 19 - encoder/mp2fixed/encoder.go | 19 - encoder/mpeg1video/encoder.go | 19 - encoder/mpeg2video/encoder.go | 19 - encoder/mpeg4/encoder.go | 19 - encoder/msmpeg4/encoder.go | 19 - encoder/msmpeg4v2/encoder.go | 19 - encoder/msvideo1/encoder.go | 19 - encoder/png/encoder.go | 19 - encoder/qtrle/encoder.go | 19 - encoder/sgi/encoder.go | 19 - encoder/tiff/encoder.go | 19 - encoder/wmav1/encoder.go | 19 - encoder/wmav2/encoder.go | 19 - encoder/wmv1/encoder.go | 19 - encoder/wmv2/encoder.go | 19 - encoder/xbm/encoder.go | 19 - error/view.go | 39 - handler/convertor.go | 178 ---- handler/convertor_anyos.go | 18 - handler/convertor_linux.go | 231 ------ handler/convertor_windows.go | 185 ----- handler/main.go | 32 - handler/menu.go | 143 ---- helper/helper.go | 11 - helper/path_separator.go | 8 - helper/path_separator_window.go | 8 - helper/prepare_background_command.go | 12 - helper/prepare_background_command_windows.go | 13 - kernel/app.go | 132 --- kernel/convertor.go | 290 ------- kernel/encoder/encoder.go | 84 -- kernel/encoder/encoders.go | 74 -- kernel/error.go | 17 - kernel/ffplay.go | 28 - kernel/items_to_convert.go | 152 ---- kernel/layout.go | 481 ----------- kernel/localizer.go | 194 ----- kernel/progressbar.go | 245 ------ kernel/queue.go | 125 --- kernel/right_tabs.go | 75 -- kernel/translations/app.en.json | 143 ---- kernel/translations/app.kk.json | 143 ---- kernel/translations/app.ru.json | 143 ---- kernel/translations/base.en.json | 45 - kernel/translations/base.kk.json | 45 - kernel/translations/base.ru.json | 45 - kernel/window.go | 89 -- localizer/view.go | 69 -- menu/view.go | 774 ------------------ menu/view_setting.go | 101 --- setting/directory_for_saving.go | 22 - setting/entity.go | 6 - setting/repository.go | 39 - theme/repository.go | 28 - theme/theme.go | 148 ---- 84 files changed, 6620 deletions(-) delete mode 100644 convertor/repository.go delete mode 100644 convertor/view.go delete mode 100644 convertor/view/conversion.go delete mode 100644 convertor/view/form_items/form.go delete mode 100644 convertor/view/form_items/h264_nvenc/view.go delete mode 100644 convertor/view/form_items/libx264/view.go delete mode 100644 convertor/view/form_items/libx265/view.go delete mode 100644 convertor/view_setting.go delete mode 100644 convertor/view_setting_button_download_ffmpeg_anyos.go delete mode 100644 convertor/view_setting_button_download_ffmpeg_linux.go delete mode 100644 convertor/view_setting_button_download_ffmpeg_windows.go delete mode 100644 encoder/apng/encoder.go delete mode 100644 encoder/bmp/encoder.go delete mode 100644 encoder/encoder.go delete mode 100644 encoder/flv/encoder.go delete mode 100644 encoder/gif/encoder.go delete mode 100644 encoder/h264_nvenc/encoder.go delete mode 100644 encoder/libmp3lame/encoder.go delete mode 100644 encoder/libshine/encoder.go delete mode 100644 encoder/libtwolame/encoder.go delete mode 100644 encoder/libvpx/encoder.go delete mode 100644 encoder/libvpx_vp9/encoder.go delete mode 100644 encoder/libwebp/encoder.go delete mode 100644 encoder/libwebp_anim/encoder.go delete mode 100644 encoder/libx264/encoder.go delete mode 100644 encoder/libx265/encoder.go delete mode 100644 encoder/libxvid/encoder.go delete mode 100644 encoder/mjpeg/encoder.go delete mode 100644 encoder/mp2/encoder.go delete mode 100644 encoder/mp2fixed/encoder.go delete mode 100644 encoder/mpeg1video/encoder.go delete mode 100644 encoder/mpeg2video/encoder.go delete mode 100644 encoder/mpeg4/encoder.go delete mode 100644 encoder/msmpeg4/encoder.go delete mode 100644 encoder/msmpeg4v2/encoder.go delete mode 100644 encoder/msvideo1/encoder.go delete mode 100644 encoder/png/encoder.go delete mode 100644 encoder/qtrle/encoder.go delete mode 100644 encoder/sgi/encoder.go delete mode 100644 encoder/tiff/encoder.go delete mode 100644 encoder/wmav1/encoder.go delete mode 100644 encoder/wmav2/encoder.go delete mode 100644 encoder/wmv1/encoder.go delete mode 100644 encoder/wmv2/encoder.go delete mode 100644 encoder/xbm/encoder.go delete mode 100644 error/view.go delete mode 100644 handler/convertor.go delete mode 100644 handler/convertor_anyos.go delete mode 100644 handler/convertor_linux.go delete mode 100644 handler/convertor_windows.go delete mode 100644 handler/main.go delete mode 100644 handler/menu.go delete mode 100644 helper/helper.go delete mode 100644 helper/path_separator.go delete mode 100644 helper/path_separator_window.go delete mode 100644 helper/prepare_background_command.go delete mode 100644 helper/prepare_background_command_windows.go delete mode 100644 kernel/app.go delete mode 100644 kernel/convertor.go delete mode 100644 kernel/encoder/encoder.go delete mode 100644 kernel/encoder/encoders.go delete mode 100644 kernel/error.go delete mode 100644 kernel/ffplay.go delete mode 100644 kernel/items_to_convert.go delete mode 100644 kernel/layout.go delete mode 100644 kernel/localizer.go delete mode 100644 kernel/progressbar.go delete mode 100644 kernel/queue.go delete mode 100644 kernel/right_tabs.go delete mode 100644 kernel/translations/app.en.json delete mode 100644 kernel/translations/app.kk.json delete mode 100644 kernel/translations/app.ru.json delete mode 100644 kernel/translations/base.en.json delete mode 100644 kernel/translations/base.kk.json delete mode 100644 kernel/translations/base.ru.json delete mode 100644 kernel/window.go delete mode 100644 localizer/view.go delete mode 100644 menu/view.go delete mode 100644 menu/view_setting.go delete mode 100644 setting/directory_for_saving.go delete mode 100644 setting/entity.go delete mode 100644 setting/repository.go delete mode 100644 theme/repository.go delete mode 100644 theme/theme.go diff --git a/convertor/repository.go b/convertor/repository.go deleted file mode 100644 index d052ff3..0000000 --- a/convertor/repository.go +++ /dev/null @@ -1,46 +0,0 @@ -package convertor - -import ( - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/setting" -) - -type RepositoryContract interface { - GetPathFfmpeg() string - SavePathFfmpeg(code string) setting.Setting - GetPathFfprobe() string - SavePathFfprobe(code string) setting.Setting - GetPathFfplay() string - SavePathFfplay(code string) setting.Setting -} - -type Repository struct { - settingRepository setting.RepositoryContract -} - -func NewRepository(settingRepository setting.RepositoryContract) *Repository { - return &Repository{settingRepository: settingRepository} -} - -func (r Repository) GetPathFfmpeg() string { - return r.settingRepository.GetValue("ffmpeg") -} - -func (r Repository) SavePathFfmpeg(path string) setting.Setting { - return r.settingRepository.CreateOrUpdate("ffmpeg", path) -} - -func (r Repository) GetPathFfprobe() string { - return r.settingRepository.GetValue("ffprobe") -} - -func (r Repository) SavePathFfprobe(path string) setting.Setting { - return r.settingRepository.CreateOrUpdate("ffprobe", path) -} - -func (r Repository) GetPathFfplay() string { - return r.settingRepository.GetValue("ffplay") -} - -func (r Repository) SavePathFfplay(path string) setting.Setting { - return r.settingRepository.CreateOrUpdate("ffplay", path) -} diff --git a/convertor/view.go b/convertor/view.go deleted file mode 100644 index e3dcb14..0000000 --- a/convertor/view.go +++ /dev/null @@ -1,60 +0,0 @@ -package convertor - -import ( - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/canvas" - "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/widget" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/convertor/view" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" - "image/color" -) - -type ViewContract interface { - Main( - formConversion view.ConversionContract, - ) - SelectFFPath( - ffmpegPath string, - ffprobePath string, - ffplayPath string, - save func(ffmpegPath string, ffprobePath string, ffplayPath string) error, - cancel func(), - donwloadFFmpeg func(progressBar *widget.ProgressBar, progressMessage *canvas.Text) error, - ) -} - -type View struct { - app kernel.AppContract - downloadFFmpeg *downloadFFmpeg -} - -type downloadFFmpeg struct { - isDownloadFFmpeg bool - blockDownloadFFmpegContainer *fyne.Container -} - -func NewView(app kernel.AppContract) *View { - return &View{ - app: app, - downloadFFmpeg: &downloadFFmpeg{ - blockDownloadFFmpegContainer: nil, - }, - } -} - -func (v View) Main(formConversion view.ConversionContract) { - converterVideoFilesTitle := v.app.GetLocalizerService().GetMessage("converterVideoFilesTitle") - v.app.GetWindow().SetContent(widget.NewCard(converterVideoFilesTitle, "", container.NewVScroll(formConversion.GetContent()))) - formConversion.AfterViewContent() -} - -func setStringErrorStyle(text *canvas.Text) { - text.Color = color.RGBA{R: 255, G: 0, B: 0, A: 255} - text.Refresh() -} - -func setStringSuccessStyle(text *canvas.Text) { - text.Color = color.RGBA{R: 49, G: 127, B: 114, A: 255} - text.Refresh() -} diff --git a/convertor/view/conversion.go b/convertor/view/conversion.go deleted file mode 100644 index 1ceeafa..0000000 --- a/convertor/view/conversion.go +++ /dev/null @@ -1,515 +0,0 @@ -package view - -import ( - "errors" - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/canvas" - "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/storage" - "fyne.io/fyne/v2/widget" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/convertor/view/form_items" - encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel/encoder" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/setting" - "image/color" - "os" - "path/filepath" -) - -type ConversionContract interface { - GetContent() fyne.CanvasObject - AfterViewContent() -} - -type Conversion struct { - 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 { - DirectoryForSave string - OverwriteOutputFiles bool - Format string - Encoder encoder2.EncoderContract -} - -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, itemsToConvertService) - directoryForSaving := newDirectoryForSaving(app, settingDirectoryForSaving) - overwriteOutputFiles := newOverwriteOutputFiles(app) - selectEncoder := newSelectEncoder(app, formats) - - items := []*widget.FormItem{ - { - Text: app.GetLocalizerService().GetMessage("fileForConversionTitle"), - Widget: fileForConversion.button, - }, - { - Widget: container.NewHScroll(fileForConversion.message), - }, - { - Text: app.GetLocalizerService().GetMessage("buttonForSelectedDirTitle"), - Widget: directoryForSaving.button, - }, - { - Widget: container.NewHScroll(directoryForSaving.message), - }, - { - Widget: overwriteOutputFiles.checkbox, - }, - { - Widget: selectEncoder.SelectFileType, - }, - { - Text: app.GetLocalizerService().GetMessage("selectFormat"), - Widget: selectEncoder.SelectFormat, - }, - { - Text: app.GetLocalizerService().GetMessage("selectEncoder"), - Widget: selectEncoder.SelectEncoder, - }, - } - form := newForm(app, items) - - return &Conversion{ - app: app, - form: form, - conversionMessage: conversionMessage, - fileForConversion: fileForConversion, - directoryForSaving: directoryForSaving, - overwriteOutputFiles: overwriteOutputFiles, - selectEncoder: selectEncoder, - runConvert: runConvert, - itemsToConvertService: itemsToConvertService, - } -} - -func (c Conversion) GetContent() fyne.CanvasObject { - c.form.form.OnSubmit = c.submit - c.fileForConversion.AddChangeCallback(c.selectFileForConversion) - c.selectEncoder.AddChangeCallback(c.changeEncoder) - if c.selectEncoder.Encoder != nil { - c.selectEncoder.SelectEncoder.SetSelectedIndex(c.selectEncoder.SelectEncoder.SelectedIndex()) - } - - return container.NewVBox( - c.form.form, - c.conversionMessage, - ) -} - -func (c Conversion) changeEncoder(encoder encoder2.EncoderContract) { - items := []*widget.FormItem{} - - if form_items.Views[encoder.GetName()] != nil { - items = form_items.Views[encoder.GetName()](encoder, c.app) - } - - c.form.ChangeItems(items) -} - -func (c Conversion) AfterViewContent() { - if len(c.itemsToConvertService.GetItems()) == 0 { - c.form.form.Disable() - } -} - -func (c Conversion) selectFileForConversion(err error) { - c.conversionMessage.Text = "" - 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("errorNoFilesAddedForConversion"))) - c.enableFormConversion() - return - } - - if len(c.directoryForSaving.path) == 0 { - showConversionMessage(c.conversionMessage, errors.New(c.app.GetLocalizerService().GetMessage("errorSelectedFolderSave"))) - c.enableFormConversion() - return - } - if len(c.selectEncoder.SelectFormat.Selected) == 0 { - showConversionMessage(c.conversionMessage, errors.New(c.app.GetLocalizerService().GetMessage("errorSelectedFormat"))) - return - } - if c.selectEncoder.Encoder == nil { - showConversionMessage(c.conversionMessage, errors.New(c.app.GetLocalizerService().GetMessage("errorSelectedEncoder"))) - return - } - c.conversionMessage.Text = "" - - c.fileForConversion.button.Disable() - c.directoryForSaving.button.Disable() - c.form.form.Disable() - - c.runConvert(HandleConvertSetting{ - DirectoryForSave: c.directoryForSaving.path, - OverwriteOutputFiles: c.overwriteOutputFiles.IsChecked(), - Format: c.selectEncoder.SelectFormat.Selected, - Encoder: c.selectEncoder.Encoder, - }) - c.enableFormConversion() - - if len(c.itemsToConvertService.GetItems()) == 0 { - c.form.form.Disable() - } -} - -func (c Conversion) enableFormConversion() { - c.fileForConversion.button.Enable() - c.directoryForSaving.button.Enable() - c.form.form.Enable() -} - -type fileForConversion struct { - button *widget.Button - message *canvas.Text - file *kernel.File - - changeCallbacks map[int]func(err error) -} - -func newFileForConversion(app kernel.AppContract, itemsToConvertService kernel.ItemsToConvertContract) *fileForConversion { - message := canvas.NewText("", color.RGBA{R: 255, G: 0, B: 0, A: 255}) - fileForConversion := &fileForConversion{ - message: message, - - changeCallbacks: map[int]func(err error){}, - } - - buttonTitle := app.GetLocalizerService().GetMessage("choose") + "\n" + - app.GetLocalizerService().GetMessage("or") + "\n" + - app.GetLocalizerService().GetMessage("dragAndDropFiles") - - 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 { - fyne.Do(func() { - fileForConversion.message.Text = err.Error() - fileForConversion.message.Refresh() - }) - fileForConversion.eventSelectFile(err) - return - } - if r == nil { - return - } - app.GetWindow().GetLayout().GetRightTabs().SelectAddedFilesTab() - - itemsToConvertService.Add(&kernel.File{ - Path: r.URI().Path(), - Name: r.URI().Name(), - Ext: r.URI().Extension(), - }) - - fileForConversion.eventSelectFile(nil) - - listableURI := storage.NewFileURI(filepath.Dir(r.URI().Path())) - locationURI, _ = storage.ListerForURI(listableURI) - }, locationURI) - }) - - app.GetWindow().SetOnDropped(func(position fyne.Position, uris []fyne.URI) { - if len(uris) == 0 { - return - } - - 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("errorDragAndDropFile") - setStringErrorStyle(fileForConversion.message) - fileForConversion.eventSelectFile(errors.New(fileForConversion.message.Text)) - } else { - fyne.Do(func() { - fileForConversion.message.Text = "" - fileForConversion.message.Refresh() - }) - } - }) - - return fileForConversion -} - -func (c fileForConversion) AddChangeCallback(callback func(err error)) { - c.changeCallbacks[len(c.changeCallbacks)] = callback -} - -func (c fileForConversion) eventSelectFile(err error) { - for _, changeCallback := range c.changeCallbacks { - changeCallback(err) - } -} - -type directoryForSaving struct { - button *widget.Button - message *canvas.Text - path string -} - -func newDirectoryForSaving(app kernel.AppContract, settingDirectoryForSaving setting.DirectoryForSavingContract) *directoryForSaving { - directoryForSaving := &directoryForSaving{ - path: "", - } - - directoryForSaving.message = canvas.NewText("", color.RGBA{R: 255, G: 0, B: 0, A: 255}) - directoryForSaving.message.TextSize = 16 - directoryForSaving.message.TextStyle = fyne.TextStyle{Bold: true} - - buttonTitle := app.GetLocalizerService().GetMessage("choose") - - var locationURI fyne.ListableURI - - location, err := getDirectoryForSaving(settingDirectoryForSaving) - if err == nil { - directoryForSaving.path = location.Path() - directoryForSaving.message.Text = location.Path() - setStringSuccessStyle(directoryForSaving.message) - } - - directoryForSaving.button = widget.NewButton(buttonTitle, func() { - app.GetWindow().NewFolderOpen(func(r fyne.ListableURI, err error) { - if err != nil { - directoryForSaving.message.Text = err.Error() - setStringErrorStyle(directoryForSaving.message) - return - } - if r == nil { - return - } - - directoryForSaving.path = r.Path() - - directoryForSaving.message.Text = r.Path() - setStringSuccessStyle(directoryForSaving.message) - locationURI, err = storage.ListerForURI(r) - - if err == nil { - _ = settingDirectoryForSaving.SaveDirectoryForSaving(locationURI.Path()) - } - - }, locationURI) - }) - - return directoryForSaving -} - -func getDirectoryForSaving(settingDirectoryForSaving setting.DirectoryForSavingContract) (fyne.ListableURI, error) { - path := settingDirectoryForSaving.GetDirectoryForSaving() - - if len(path) > 0 { - path = "file://" + path - } - - uri, err := storage.ParseURI(path) - if err != nil { - return nil, err - } - - return storage.ListerForURI(uri) -} - -type overwriteOutputFiles struct { - checkbox *widget.Check - isChecked bool -} - -func newOverwriteOutputFiles(app kernel.AppContract) *overwriteOutputFiles { - overwriteOutputFiles := &overwriteOutputFiles{ - isChecked: false, - } - checkboxOverwriteOutputFilesTitle := app.GetLocalizerService().GetMessage("checkboxOverwriteOutputFilesTitle") - overwriteOutputFiles.checkbox = widget.NewCheck(checkboxOverwriteOutputFilesTitle, func(b bool) { - overwriteOutputFiles.isChecked = b - }) - - return overwriteOutputFiles -} - -func (receiver overwriteOutputFiles) IsChecked() bool { - return receiver.isChecked -} - -type selectEncoder struct { - SelectFileType *widget.RadioGroup - SelectFormat *widget.Select - SelectEncoder *widget.Select - Encoder encoder2.EncoderContract - - changeCallbacks map[int]func(encoder encoder2.EncoderContract) -} - -func newSelectEncoder(app kernel.AppContract, formats encoder.ConvertorFormatsContract) *selectEncoder { - selectEncoder := &selectEncoder{ - changeCallbacks: map[int]func(encoder encoder2.EncoderContract){}, - } - - encoders := map[int]encoder2.EncoderDataContract{} - selectEncoder.SelectEncoder = widget.NewSelect([]string{}, func(s string) { - if encoders[selectEncoder.SelectEncoder.SelectedIndex()] == nil { - return - } - selectEncoderData := encoders[selectEncoder.SelectEncoder.SelectedIndex()] - selectEncoder.ChangeEncoder(selectEncoderData.NewEncoder()) - }) - - formatSelected := "" - selectEncoder.SelectFormat = widget.NewSelect([]string{}, func(s string) { - if formatSelected == s { - return - } - formatSelected = s - format, err := formats.GetFormat(s) - if err != nil { - return - } - encoderOptions := []string{} - encoders = map[int]encoder2.EncoderDataContract{} - for _, e := range format.GetEncoders() { - encoders[len(encoders)] = e - encoderOptions = append(encoderOptions, app.GetLocalizerService().GetMessage("encoder_"+e.GetTitle())) - } - selectEncoder.SelectEncoder.SetOptions(encoderOptions) - selectEncoder.SelectEncoder.SetSelectedIndex(0) - }) - - fileTypeOptions := []string{} - for _, fileType := range encoder2.GetListFileType() { - fileTypeOptions = append(fileTypeOptions, fileType.Name()) - } - - encoderGroupVideo := app.GetLocalizerService().GetMessage("encoderGroupVideo") - encoderGroupAudio := app.GetLocalizerService().GetMessage("encoderGroupAudio") - encoderGroupImage := app.GetLocalizerService().GetMessage("encoderGroupImage") - encoderGroup := map[string]string{ - encoderGroupVideo: "video", - encoderGroupAudio: "audio", - encoderGroupImage: "image", - } - selectEncoder.SelectFileType = widget.NewRadioGroup([]string{encoderGroupVideo, encoderGroupAudio, encoderGroupImage}, func(s string) { - groupCode := encoderGroup[s] - - formatOptions := []string{} - for _, f := range formats.GetFormats() { - if groupCode != f.GetFileType().Name() { - continue - } - formatOptions = append(formatOptions, f.GetTitle()) - } - selectEncoder.SelectFormat.SetOptions(formatOptions) - if groupCode == encoder2.FileType(encoder2.Video).Name() { - selectEncoder.SelectFormat.SetSelected("mp4") - } else { - selectEncoder.SelectFormat.SetSelectedIndex(0) - } - }) - selectEncoder.SelectFileType.Horizontal = true - selectEncoder.SelectFileType.Required = true - selectEncoder.SelectFileType.SetSelected(encoderGroupVideo) - - return selectEncoder -} - -func (e *selectEncoder) ChangeEncoder(encoder encoder2.EncoderContract) { - e.Encoder = encoder - e.eventSelectEncoder(e.Encoder) -} - -func (e *selectEncoder) AddChangeCallback(callback func(encoder encoder2.EncoderContract)) { - e.changeCallbacks[len(e.changeCallbacks)] = callback -} - -func (e *selectEncoder) eventSelectEncoder(encoder encoder2.EncoderContract) { - for _, changeCallback := range e.changeCallbacks { - changeCallback(encoder) - } -} - -func setStringErrorStyle(text *canvas.Text) { - text.Color = color.RGBA{R: 255, G: 0, B: 0, A: 255} - text.Refresh() -} - -func setStringSuccessStyle(text *canvas.Text) { - text.Color = color.RGBA{R: 49, G: 127, B: 114, A: 255} - text.Refresh() -} - -func showConversionMessage(conversionMessage *canvas.Text, err error) { - conversionMessage.Text = err.Error() - setStringErrorStyle(conversionMessage) -} - -type form struct { - form *widget.Form - items []*widget.FormItem -} - -func newForm(app kernel.AppContract, items []*widget.FormItem) *form { - f := widget.NewForm() - f.SubmitText = app.GetLocalizerService().GetMessage("converterVideoFilesSubmitTitle") - f.Items = items - - return &form{ - form: f, - items: items, - } -} - -func (f form) ChangeItems(items []*widget.FormItem) { - f.form.Items = f.items - f.form.Refresh() - f.form.Items = append(f.form.Items, items...) - f.form.Refresh() -} diff --git a/convertor/view/form_items/form.go b/convertor/view/form_items/form.go deleted file mode 100644 index c0d1a7d..0000000 --- a/convertor/view/form_items/form.go +++ /dev/null @@ -1,16 +0,0 @@ -package form_items - -import ( - "fyne.io/fyne/v2/widget" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/convertor/view/form_items/h264_nvenc" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/convertor/view/form_items/libx264" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/convertor/view/form_items/libx265" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" -) - -var Views = map[string]func(encoder encoder.EncoderContract, app kernel.AppContract) []*widget.FormItem{ - "libx264": libx264.View, - "h264_nvenc": h264_nvenc.View, - "libx265": libx265.View, -} diff --git a/convertor/view/form_items/h264_nvenc/view.go b/convertor/view/form_items/h264_nvenc/view.go deleted file mode 100644 index 2f92333..0000000 --- a/convertor/view/form_items/h264_nvenc/view.go +++ /dev/null @@ -1,64 +0,0 @@ -package h264_nvenc - -import ( - "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/widget" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/h264_nvenc" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" -) - -func View(encoder encoder.EncoderContract, app kernel.AppContract) []*widget.FormItem { - items := []*widget.FormItem{} - - items = append(items, presetParameter(encoder, app)...) - - return items -} - -func presetParameter(encoder encoder.EncoderContract, app kernel.AppContract) []*widget.FormItem { - parameter, err := encoder.GetParameter("preset") - if err != nil { - return nil - } - - presets := map[string]string{} - presetsForSelect := []string{} - presetDefault := "" - - for _, name := range h264_nvenc.Presets { - title := name - presetsForSelect = append(presetsForSelect, name) - presets[title] = name - if name == parameter.Get() { - presetDefault = title - } - } - - elementSelect := widget.NewSelect(presetsForSelect, func(s string) { - if presets[s] == "" { - return - } - parameter.Set(presets[s]) - }) - elementSelect.SetSelected(presetDefault) - elementSelect.Hide() - - checkboxTitle := app.GetLocalizerService().GetMessage("parameterCheckbox") - elementCheckbox := widget.NewCheck(checkboxTitle, func(b bool) { - if b == true { - parameter.SetEnable() - elementSelect.Show() - return - } - parameter.SetDisable() - elementSelect.Hide() - }) - - return []*widget.FormItem{ - { - Text: app.GetLocalizerService().GetMessage("formPreset"), - Widget: container.NewVBox(elementCheckbox, elementSelect), - }, - } -} diff --git a/convertor/view/form_items/libx264/view.go b/convertor/view/form_items/libx264/view.go deleted file mode 100644 index dd8ae34..0000000 --- a/convertor/view/form_items/libx264/view.go +++ /dev/null @@ -1,64 +0,0 @@ -package libx264 - -import ( - "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/widget" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/libx264" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" -) - -func View(encoder encoder.EncoderContract, app kernel.AppContract) []*widget.FormItem { - items := []*widget.FormItem{} - - items = append(items, presetParameter(encoder, app)...) - - return items -} - -func presetParameter(encoder encoder.EncoderContract, app kernel.AppContract) []*widget.FormItem { - parameter, err := encoder.GetParameter("preset") - if err != nil { - return nil - } - - presets := map[string]string{} - presetsForSelect := []string{} - presetDefault := "" - - for _, name := range libx264.Presets { - title := app.GetLocalizerService().GetMessage("preset_" + name) - presetsForSelect = append(presetsForSelect, title) - presets[title] = name - if name == parameter.Get() { - presetDefault = title - } - } - - elementSelect := widget.NewSelect(presetsForSelect, func(s string) { - if presets[s] == "" { - return - } - parameter.Set(presets[s]) - }) - elementSelect.SetSelected(presetDefault) - elementSelect.Hide() - - checkboxTitle := app.GetLocalizerService().GetMessage("parameterCheckbox") - elementCheckbox := widget.NewCheck(checkboxTitle, func(b bool) { - if b == true { - parameter.SetEnable() - elementSelect.Show() - return - } - parameter.SetDisable() - elementSelect.Hide() - }) - - return []*widget.FormItem{ - { - Text: app.GetLocalizerService().GetMessage("formPreset"), - Widget: container.NewVBox(elementCheckbox, elementSelect), - }, - } -} diff --git a/convertor/view/form_items/libx265/view.go b/convertor/view/form_items/libx265/view.go deleted file mode 100644 index ae71d4d..0000000 --- a/convertor/view/form_items/libx265/view.go +++ /dev/null @@ -1,64 +0,0 @@ -package libx265 - -import ( - "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/widget" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/libx265" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" -) - -func View(encoder encoder.EncoderContract, app kernel.AppContract) []*widget.FormItem { - items := []*widget.FormItem{} - - items = append(items, presetParameter(encoder, app)...) - - return items -} - -func presetParameter(encoder encoder.EncoderContract, app kernel.AppContract) []*widget.FormItem { - parameter, err := encoder.GetParameter("preset") - if err != nil { - return nil - } - - presets := map[string]string{} - presetsForSelect := []string{} - presetDefault := "" - - for _, name := range libx265.Presets { - title := app.GetLocalizerService().GetMessage("preset_" + name) - presetsForSelect = append(presetsForSelect, title) - presets[title] = name - if name == parameter.Get() { - presetDefault = title - } - } - - elementSelect := widget.NewSelect(presetsForSelect, func(s string) { - if presets[s] == "" { - return - } - parameter.Set(presets[s]) - }) - elementSelect.SetSelected(presetDefault) - elementSelect.Hide() - - checkboxTitle := app.GetLocalizerService().GetMessage("parameterCheckbox") - elementCheckbox := widget.NewCheck(checkboxTitle, func(b bool) { - if b == true { - parameter.SetEnable() - elementSelect.Show() - return - } - parameter.SetDisable() - elementSelect.Hide() - }) - - return []*widget.FormItem{ - { - Text: app.GetLocalizerService().GetMessage("formPreset"), - Widget: container.NewVBox(elementCheckbox, elementSelect), - }, - } -} diff --git a/convertor/view_setting.go b/convertor/view_setting.go deleted file mode 100644 index b18a070..0000000 --- a/convertor/view_setting.go +++ /dev/null @@ -1,128 +0,0 @@ -package convertor - -import ( - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/canvas" - "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/storage" - "fyne.io/fyne/v2/widget" - "image/color" - "net/url" - "path/filepath" -) - -func (v View) SelectFFPath( - currentPathFfmpeg string, - currentPathFfprobe string, - currentPathFfplay string, - save func(ffmpegPath string, ffprobePath string, ffplayPath string) error, - cancel func(), - donwloadFFmpeg func(progressBar *widget.ProgressBar, progressMessage *canvas.Text) error, -) { - errorMessage := canvas.NewText("", color.RGBA{R: 255, G: 0, B: 0, A: 255}) - errorMessage.TextSize = 16 - errorMessage.TextStyle = fyne.TextStyle{Bold: true} - - 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", - Host: "ffmpeg.org", - Path: "download.html", - }) - - form := &widget.Form{ - Items: []*widget.FormItem{ - { - Text: v.app.GetLocalizerService().GetMessage("titleDownloadLink"), - Widget: link, - }, - { - Text: v.app.GetLocalizerService().GetMessage("pathToFfmpeg"), - Widget: buttonFFmpeg, - }, - { - Widget: container.NewHScroll(buttonFFmpegMessage), - }, - { - Text: v.app.GetLocalizerService().GetMessage("pathToFfprobe"), - Widget: buttonFFprobe, - }, - { - Widget: container.NewHScroll(buttonFFprobeMessage), - }, - { - Text: v.app.GetLocalizerService().GetMessage("pathToFfplay"), - Widget: buttonFFplay, - }, - { - Widget: container.NewHScroll(buttonFFplayMessage), - }, - { - Widget: errorMessage, - }, - }, - SubmitText: v.app.GetLocalizerService().GetMessage("save"), - OnSubmit: func() { - err := save(*ffmpegPath, *ffprobePath, *ffplayPath) - if err != nil { - errorMessage.Text = err.Error() - } - }, - } - if cancel != nil { - form.OnCancel = cancel - form.CancelText = v.app.GetLocalizerService().GetMessage("cancel") - } - selectFFPathTitle := v.app.GetLocalizerService().GetMessage("selectFFPathTitle") - - if v.downloadFFmpeg.blockDownloadFFmpegContainer == nil { - v.downloadFFmpeg.blockDownloadFFmpegContainer = v.blockDownloadFFmpeg(donwloadFFmpeg) - } - - v.app.GetWindow().SetContent(widget.NewCard(selectFFPathTitle, "", container.NewVBox( - form, - v.downloadFFmpeg.blockDownloadFFmpegContainer, - ))) -} - -func (v View) getButtonSelectFile(path string) (filePath *string, button *widget.Button, buttonMessage *canvas.Text) { - filePath = &path - - buttonMessage = canvas.NewText(path, color.RGBA{R: 49, G: 127, B: 114, A: 255}) - buttonMessage.TextSize = 16 - buttonMessage.TextStyle = fyne.TextStyle{Bold: true} - - buttonTitle := v.app.GetLocalizerService().GetMessage("choose") - - var locationURI fyne.ListableURI - if len(path) > 0 { - listableURI := storage.NewFileURI(filepath.Dir(path)) - locationURI, _ = storage.ListerForURI(listableURI) - } - - button = widget.NewButton(buttonTitle, func() { - v.app.GetWindow().NewFileOpen(func(r fyne.URIReadCloser, err error) { - if err != nil { - buttonMessage.Text = err.Error() - setStringErrorStyle(buttonMessage) - return - } - if r == nil { - return - } - - path = r.URI().Path() - - buttonMessage.Text = r.URI().Path() - setStringSuccessStyle(buttonMessage) - - listableURI := storage.NewFileURI(filepath.Dir(r.URI().Path())) - locationURI, _ = storage.ListerForURI(listableURI) - }, locationURI) - }) - - return -} diff --git a/convertor/view_setting_button_download_ffmpeg_anyos.go b/convertor/view_setting_button_download_ffmpeg_anyos.go deleted file mode 100644 index 794fd91..0000000 --- a/convertor/view_setting_button_download_ffmpeg_anyos.go +++ /dev/null @@ -1,17 +0,0 @@ -//go:build !windows && !linux -// +build !windows,!linux - -package convertor - -import ( - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/canvas" - "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/widget" -) - -func (v View) blockDownloadFFmpeg( - donwloadFFmpeg func(progressBar *widget.ProgressBar, progressMessage *canvas.Text) error, -) *fyne.Container { - return container.NewVBox() -} diff --git a/convertor/view_setting_button_download_ffmpeg_linux.go b/convertor/view_setting_button_download_ffmpeg_linux.go deleted file mode 100644 index d08900c..0000000 --- a/convertor/view_setting_button_download_ffmpeg_linux.go +++ /dev/null @@ -1,61 +0,0 @@ -//go:build linux -// +build linux - -package convertor - -import ( - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/canvas" - "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/widget" - "golang.org/x/image/colornames" - "image/color" -) - -func (v View) blockDownloadFFmpeg( - donwloadFFmpeg func(progressBar *widget.ProgressBar, progressMessage *canvas.Text) error, -) *fyne.Container { - - errorDownloadFFmpegMessage := canvas.NewText("", color.RGBA{R: 255, G: 0, B: 0, A: 255}) - errorDownloadFFmpegMessage.TextSize = 16 - errorDownloadFFmpegMessage.TextStyle = fyne.TextStyle{Bold: true} - - progressDownloadFFmpegMessage := canvas.NewText("", color.RGBA{R: 49, G: 127, B: 114, A: 255}) - progressDownloadFFmpegMessage.TextSize = 16 - progressDownloadFFmpegMessage.TextStyle = fyne.TextStyle{Bold: true} - - progressBar := widget.NewProgressBar() - - var buttonDownloadFFmpeg *widget.Button - - buttonDownloadFFmpeg = widget.NewButton(v.app.GetLocalizerService().GetMessage("download"), func() { - fyne.Do(func() { - buttonDownloadFFmpeg.Disable() - }) - go func() { - err := donwloadFFmpeg(progressBar, progressDownloadFFmpegMessage) - if err != nil { - errorDownloadFFmpegMessage.Text = err.Error() - } - fyne.Do(func() { - buttonDownloadFFmpeg.Enable() - }) - }() - - }) - - downloadFFmpegFromSiteMessage := v.app.GetLocalizerService().GetMessage("downloadFFmpegFromSite") - - return container.NewVBox( - canvas.NewLine(colornames.Darkgreen), - widget.NewCard(v.app.GetLocalizerService().GetMessage("buttonDownloadFFmpeg"), "", container.NewVBox( - widget.NewRichTextFromMarkdown( - downloadFFmpegFromSiteMessage+" [https://github.com/BtbN/FFmpeg-Builds/releases](https://github.com/BtbN/FFmpeg-Builds/releases)", - ), - buttonDownloadFFmpeg, - errorDownloadFFmpegMessage, - progressDownloadFFmpegMessage, - progressBar, - )), - ) -} diff --git a/convertor/view_setting_button_download_ffmpeg_windows.go b/convertor/view_setting_button_download_ffmpeg_windows.go deleted file mode 100644 index b71deef..0000000 --- a/convertor/view_setting_button_download_ffmpeg_windows.go +++ /dev/null @@ -1,61 +0,0 @@ -//go:build windows -// +build windows - -package convertor - -import ( - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/canvas" - "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/widget" - "golang.org/x/image/colornames" - "image/color" -) - -func (v View) blockDownloadFFmpeg( - donwloadFFmpeg func(progressBar *widget.ProgressBar, progressMessage *canvas.Text) error, -) *fyne.Container { - - errorDownloadFFmpegMessage := canvas.NewText("", color.RGBA{R: 255, G: 0, B: 0, A: 255}) - errorDownloadFFmpegMessage.TextSize = 16 - errorDownloadFFmpegMessage.TextStyle = fyne.TextStyle{Bold: true} - - progressDownloadFFmpegMessage := canvas.NewText("", color.RGBA{R: 49, G: 127, B: 114, A: 255}) - progressDownloadFFmpegMessage.TextSize = 16 - progressDownloadFFmpegMessage.TextStyle = fyne.TextStyle{Bold: true} - - progressBar := widget.NewProgressBar() - - var buttonDownloadFFmpeg *widget.Button - - buttonDownloadFFmpeg = widget.NewButton(v.app.GetLocalizerService().GetMessage("download"), func() { - - go func() { - fyne.Do(func() { - buttonDownloadFFmpeg.Disable() - }) - err := donwloadFFmpeg(progressBar, progressDownloadFFmpegMessage) - if err != nil { - errorDownloadFFmpegMessage.Text = err.Error() - } - fyne.Do(func() { - buttonDownloadFFmpeg.Enable() - }) - }() - }) - - downloadFFmpegFromSiteMessage := v.app.GetLocalizerService().GetMessage("downloadFFmpegFromSite") - - return container.NewVBox( - canvas.NewLine(colornames.Darkgreen), - widget.NewCard(v.app.GetLocalizerService().GetMessage("buttonDownloadFFmpeg"), "", container.NewVBox( - widget.NewRichTextFromMarkdown( - downloadFFmpegFromSiteMessage+" [https://github.com/BtbN/FFmpeg-Builds/releases](https://github.com/BtbN/FFmpeg-Builds/releases)", - ), - buttonDownloadFFmpeg, - errorDownloadFFmpegMessage, - progressDownloadFFmpegMessage, - progressBar, - )), - ) -} diff --git a/encoder/apng/encoder.go b/encoder/apng/encoder.go deleted file mode 100644 index c43b48a..0000000 --- a/encoder/apng/encoder.go +++ /dev/null @@ -1,19 +0,0 @@ -package apng - -import encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - -func NewEncoder() encoder2.EncoderContract { - parameters := map[string]encoder2.ParameterContract{} - getParams := func(parameters map[string]encoder2.ParameterContract) []string { - return []string{"-c:v", "apng"} - } - - return encoder2.NewEncoder("apng", parameters, getParams) -} - -func NewData() encoder2.EncoderDataContract { - title := "apng" - formats := []string{"apng"} - fileType := encoder2.FileType(encoder2.Image) - return encoder2.NewData(title, formats, fileType, NewEncoder) -} diff --git a/encoder/bmp/encoder.go b/encoder/bmp/encoder.go deleted file mode 100644 index f72a6d9..0000000 --- a/encoder/bmp/encoder.go +++ /dev/null @@ -1,19 +0,0 @@ -package bmp - -import encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - -func NewEncoder() encoder2.EncoderContract { - parameters := map[string]encoder2.ParameterContract{} - getParams := func(parameters map[string]encoder2.ParameterContract) []string { - return []string{"-c:v", "bmp"} - } - - return encoder2.NewEncoder("bmp", parameters, getParams) -} - -func NewData() encoder2.EncoderDataContract { - title := "bmp" - formats := []string{"bmp"} - fileType := encoder2.FileType(encoder2.Image) - return encoder2.NewData(title, formats, fileType, NewEncoder) -} diff --git a/encoder/encoder.go b/encoder/encoder.go deleted file mode 100644 index 7aedf47..0000000 --- a/encoder/encoder.go +++ /dev/null @@ -1,169 +0,0 @@ -package encoder - -import "errors" - -type EncoderContract interface { - GetName() string - GetParams() []string - GetParameter(name string) (ParameterContract, error) -} - -type ParameterContract interface { - GetName() string - Set(string) error - Get() string - IsEnabled() bool - SetEnable() - SetDisable() -} - -type EncoderDataContract interface { - GetTitle() string - GetFormats() []string - GetFileType() FileTypeContract - NewEncoder() EncoderContract -} - -type Data struct { - title string - formats []string - fileType FileTypeContract - encoder func() EncoderContract -} - -func NewData(title string, formats []string, fileType FileTypeContract, encoder func() EncoderContract) *Data { - return &Data{ - title: title, - formats: formats, - fileType: fileType, - encoder: encoder, - } -} - -func (data Data) GetTitle() string { - return data.title -} - -func (data Data) GetFormats() []string { - return data.formats -} - -func (data Data) NewEncoder() EncoderContract { - return data.encoder() -} - -func (data Data) GetFileType() FileTypeContract { - return data.fileType -} - -type FileTypeContract interface { - Name() string - Ordinal() int -} - -const ( - Video = iota - Audio - Image -) - -type FileType uint - -var fileTypeStrings = []string{ - "video", - "audio", - "image", -} - -func (fileType FileType) Name() string { - return fileTypeStrings[fileType] -} - -func (fileType FileType) Ordinal() int { - return int(fileType) -} - -func GetListFileType() []FileTypeContract { - return []FileTypeContract{ - FileType(Video), - FileType(Audio), - FileType(Image), - } -} - -type Encoder struct { - name string - parameters map[string]ParameterContract - getParams func(parameters map[string]ParameterContract) []string -} - -func NewEncoder(name string, parameters map[string]ParameterContract, getParams func(parameters map[string]ParameterContract) []string) *Encoder { - return &Encoder{ - name: name, - parameters: parameters, - getParams: getParams, - } -} - -func (e *Encoder) GetName() string { - return e.name -} - -func (e *Encoder) GetParams() []string { - return e.getParams(e.parameters) -} - -func (e *Encoder) GetParameter(name string) (ParameterContract, error) { - if e.parameters[name] == nil { - return nil, errors.New("parameter not found") - } - - return e.parameters[name], nil -} - -type Parameter struct { - name string - isEnabled bool - parameter string - setParameter func(string) (string, error) -} - -func NewParameter(name string, isEnabled bool, defaultParameter string, setParameter func(string) (string, error)) *Parameter { - return &Parameter{ - name: name, - isEnabled: isEnabled, - parameter: defaultParameter, - setParameter: setParameter, - } -} - -func (p *Parameter) GetName() string { - return p.name -} - -func (p *Parameter) Set(s string) (err error) { - if p.setParameter != nil { - s, err = p.setParameter(s) - if err != nil { - return err - } - } - p.parameter = s - return nil -} - -func (p *Parameter) Get() string { - return p.parameter -} - -func (p *Parameter) IsEnabled() bool { - return p.isEnabled -} - -func (p *Parameter) SetEnable() { - p.isEnabled = true -} - -func (p *Parameter) SetDisable() { - p.isEnabled = false -} diff --git a/encoder/flv/encoder.go b/encoder/flv/encoder.go deleted file mode 100644 index 7c2c8e4..0000000 --- a/encoder/flv/encoder.go +++ /dev/null @@ -1,19 +0,0 @@ -package flv - -import encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - -func NewEncoder() encoder2.EncoderContract { - parameters := map[string]encoder2.ParameterContract{} - getParams := func(parameters map[string]encoder2.ParameterContract) []string { - return []string{"-c:v", "flv"} - } - - return encoder2.NewEncoder("flv", parameters, getParams) -} - -func NewData() encoder2.EncoderDataContract { - title := "flv" - formats := []string{"flv"} - fileType := encoder2.FileType(encoder2.Video) - return encoder2.NewData(title, formats, fileType, NewEncoder) -} diff --git a/encoder/gif/encoder.go b/encoder/gif/encoder.go deleted file mode 100644 index d138ae1..0000000 --- a/encoder/gif/encoder.go +++ /dev/null @@ -1,19 +0,0 @@ -package gif - -import encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - -func NewEncoder() encoder2.EncoderContract { - parameters := map[string]encoder2.ParameterContract{} - getParams := func(parameters map[string]encoder2.ParameterContract) []string { - return []string{"-c:v", "gif"} - } - - return encoder2.NewEncoder("gif", parameters, getParams) -} - -func NewData() encoder2.EncoderDataContract { - title := "gif" - formats := []string{"gif"} - fileType := encoder2.FileType(encoder2.Image) - return encoder2.NewData(title, formats, fileType, NewEncoder) -} diff --git a/encoder/h264_nvenc/encoder.go b/encoder/h264_nvenc/encoder.go deleted file mode 100644 index b064975..0000000 --- a/encoder/h264_nvenc/encoder.go +++ /dev/null @@ -1,58 +0,0 @@ -package h264_nvenc - -import ( - "errors" - encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" -) - -var Presets = []string{ - "default", - "slow", - "medium", - "fast", - "hp", - "hq", - "bd", - "ll", - "llhq", - "llhp", - "lossless", - "losslesshp", -} - -func NewEncoder() encoder2.EncoderContract { - parameters := map[string]encoder2.ParameterContract{ - "preset": newParameterPreset(), - } - getParams := func(parameters map[string]encoder2.ParameterContract) []string { - params := []string{"-c:v", "h264_nvenc"} - - if parameters["preset"] != nil && parameters["preset"].IsEnabled() { - params = append(params, "-preset", parameters["preset"].Get()) - } - - return params - } - - return encoder2.NewEncoder("h264_nvenc", parameters, getParams) -} - -func NewData() encoder2.EncoderDataContract { - title := "h264_nvenc" - formats := []string{"mp4"} - fileType := encoder2.FileType(encoder2.Video) - return encoder2.NewData(title, formats, fileType, NewEncoder) -} - -func newParameterPreset() encoder2.ParameterContract { - setParameter := func(s string) (string, error) { - for _, value := range Presets { - if value == s { - return value, nil - } - } - - return "", errors.New("preset not found") - } - return encoder2.NewParameter("preset", false, "default", setParameter) -} diff --git a/encoder/libmp3lame/encoder.go b/encoder/libmp3lame/encoder.go deleted file mode 100644 index 33c2f1a..0000000 --- a/encoder/libmp3lame/encoder.go +++ /dev/null @@ -1,19 +0,0 @@ -package libmp3lame - -import encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - -func NewEncoder() encoder2.EncoderContract { - parameters := map[string]encoder2.ParameterContract{} - getParams := func(parameters map[string]encoder2.ParameterContract) []string { - return []string{"-c:a", "libmp3lame"} - } - - return encoder2.NewEncoder("libmp3lame", parameters, getParams) -} - -func NewData() encoder2.EncoderDataContract { - title := "libmp3lame" - formats := []string{"mp3"} - fileType := encoder2.FileType(encoder2.Audio) - return encoder2.NewData(title, formats, fileType, NewEncoder) -} diff --git a/encoder/libshine/encoder.go b/encoder/libshine/encoder.go deleted file mode 100644 index d7859b4..0000000 --- a/encoder/libshine/encoder.go +++ /dev/null @@ -1,19 +0,0 @@ -package libshine - -import encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - -func NewEncoder() encoder2.EncoderContract { - parameters := map[string]encoder2.ParameterContract{} - getParams := func(parameters map[string]encoder2.ParameterContract) []string { - return []string{"-c:a", "libshine"} - } - - return encoder2.NewEncoder("libshine", parameters, getParams) -} - -func NewData() encoder2.EncoderDataContract { - title := "libshine" - formats := []string{"mp3"} - fileType := encoder2.FileType(encoder2.Audio) - return encoder2.NewData(title, formats, fileType, NewEncoder) -} diff --git a/encoder/libtwolame/encoder.go b/encoder/libtwolame/encoder.go deleted file mode 100644 index 87f5ba4..0000000 --- a/encoder/libtwolame/encoder.go +++ /dev/null @@ -1,19 +0,0 @@ -package libtwolame - -import encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - -func NewEncoder() encoder2.EncoderContract { - parameters := map[string]encoder2.ParameterContract{} - getParams := func(parameters map[string]encoder2.ParameterContract) []string { - return []string{"-c:a", "libtwolame"} - } - - return encoder2.NewEncoder("libtwolame", parameters, getParams) -} - -func NewData() encoder2.EncoderDataContract { - title := "libtwolame" - formats := []string{"mp2"} - fileType := encoder2.FileType(encoder2.Audio) - return encoder2.NewData(title, formats, fileType, NewEncoder) -} diff --git a/encoder/libvpx/encoder.go b/encoder/libvpx/encoder.go deleted file mode 100644 index 86ab780..0000000 --- a/encoder/libvpx/encoder.go +++ /dev/null @@ -1,19 +0,0 @@ -package libvpx - -import encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - -func NewEncoder() encoder2.EncoderContract { - parameters := map[string]encoder2.ParameterContract{} - getParams := func(parameters map[string]encoder2.ParameterContract) []string { - return []string{"-c:v", "libvpx"} - } - - return encoder2.NewEncoder("libvpx", parameters, getParams) -} - -func NewData() encoder2.EncoderDataContract { - title := "libvpx" - formats := []string{"webm", "mkv"} - fileType := encoder2.FileType(encoder2.Video) - return encoder2.NewData(title, formats, fileType, NewEncoder) -} diff --git a/encoder/libvpx_vp9/encoder.go b/encoder/libvpx_vp9/encoder.go deleted file mode 100644 index 00cecc3..0000000 --- a/encoder/libvpx_vp9/encoder.go +++ /dev/null @@ -1,19 +0,0 @@ -package libvpx_vp9 - -import encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - -func NewEncoder() encoder2.EncoderContract { - parameters := map[string]encoder2.ParameterContract{} - getParams := func(parameters map[string]encoder2.ParameterContract) []string { - return []string{"-c:v", "libvpx-vp9"} - } - - return encoder2.NewEncoder("libvpx_vp9", parameters, getParams) -} - -func NewData() encoder2.EncoderDataContract { - title := "libvpx-vp9" - formats := []string{"webm", "mkv"} - fileType := encoder2.FileType(encoder2.Video) - return encoder2.NewData(title, formats, fileType, NewEncoder) -} diff --git a/encoder/libwebp/encoder.go b/encoder/libwebp/encoder.go deleted file mode 100644 index b324503..0000000 --- a/encoder/libwebp/encoder.go +++ /dev/null @@ -1,19 +0,0 @@ -package libwebp - -import encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - -func NewEncoder() encoder2.EncoderContract { - parameters := map[string]encoder2.ParameterContract{} - getParams := func(parameters map[string]encoder2.ParameterContract) []string { - return []string{"-c:v", "libwebp"} - } - - return encoder2.NewEncoder("libwebp", parameters, getParams) -} - -func NewData() encoder2.EncoderDataContract { - title := "libwebp" - formats := []string{"webp"} - fileType := encoder2.FileType(encoder2.Image) - return encoder2.NewData(title, formats, fileType, NewEncoder) -} diff --git a/encoder/libwebp_anim/encoder.go b/encoder/libwebp_anim/encoder.go deleted file mode 100644 index 0a6fcc5..0000000 --- a/encoder/libwebp_anim/encoder.go +++ /dev/null @@ -1,19 +0,0 @@ -package libwebp_anim - -import encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - -func NewEncoder() encoder2.EncoderContract { - parameters := map[string]encoder2.ParameterContract{} - getParams := func(parameters map[string]encoder2.ParameterContract) []string { - return []string{"-c:v", "libwebp_anim"} - } - - return encoder2.NewEncoder("libwebp_anim", parameters, getParams) -} - -func NewData() encoder2.EncoderDataContract { - title := "libwebp_anim" - formats := []string{"webp"} - fileType := encoder2.FileType(encoder2.Image) - return encoder2.NewData(title, formats, fileType, NewEncoder) -} diff --git a/encoder/libx264/encoder.go b/encoder/libx264/encoder.go deleted file mode 100644 index 5543570..0000000 --- a/encoder/libx264/encoder.go +++ /dev/null @@ -1,56 +0,0 @@ -package libx264 - -import ( - "errors" - encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" -) - -var Presets = []string{ - "ultrafast", - "superfast", - "veryfast", - "faster", - "fast", - "medium", - "slow", - "slower", - "veryslow", - "placebo", -} - -func NewEncoder() encoder2.EncoderContract { - parameters := map[string]encoder2.ParameterContract{ - "preset": newParameterPreset(), - } - getParams := func(parameters map[string]encoder2.ParameterContract) []string { - params := []string{"-c:v", "libx264"} - - if parameters["preset"] != nil && parameters["preset"].IsEnabled() { - params = append(params, "-preset", parameters["preset"].Get()) - } - - return params - } - - return encoder2.NewEncoder("libx264", parameters, getParams) -} - -func NewData() encoder2.EncoderDataContract { - title := "libx264" - formats := []string{"mp4"} - fileType := encoder2.FileType(encoder2.Video) - return encoder2.NewData(title, formats, fileType, NewEncoder) -} - -func newParameterPreset() encoder2.ParameterContract { - setParameter := func(s string) (string, error) { - for _, value := range Presets { - if value == s { - return value, nil - } - } - - return "", errors.New("preset not found") - } - return encoder2.NewParameter("preset", false, "medium", setParameter) -} diff --git a/encoder/libx265/encoder.go b/encoder/libx265/encoder.go deleted file mode 100644 index 1d4a1f3..0000000 --- a/encoder/libx265/encoder.go +++ /dev/null @@ -1,56 +0,0 @@ -package libx265 - -import ( - "errors" - encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" -) - -var Presets = []string{ - "ultrafast", - "superfast", - "veryfast", - "faster", - "fast", - "medium", - "slow", - "slower", - "veryslow", - "placebo", -} - -func NewEncoder() encoder2.EncoderContract { - parameters := map[string]encoder2.ParameterContract{ - "preset": newParameterPreset(), - } - getParams := func(parameters map[string]encoder2.ParameterContract) []string { - params := []string{"-c:v", "libx265"} - - if parameters["preset"] != nil && parameters["preset"].IsEnabled() { - params = append(params, "-preset", parameters["preset"].Get()) - } - - return params - } - - return encoder2.NewEncoder("libx265", parameters, getParams) -} - -func NewData() encoder2.EncoderDataContract { - title := "libx265" - formats := []string{"mp4"} - fileType := encoder2.FileType(encoder2.Video) - return encoder2.NewData(title, formats, fileType, NewEncoder) -} - -func newParameterPreset() encoder2.ParameterContract { - setParameter := func(s string) (string, error) { - for _, value := range Presets { - if value == s { - return value, nil - } - } - - return "", errors.New("preset not found") - } - return encoder2.NewParameter("preset", false, "medium", setParameter) -} diff --git a/encoder/libxvid/encoder.go b/encoder/libxvid/encoder.go deleted file mode 100644 index fca496a..0000000 --- a/encoder/libxvid/encoder.go +++ /dev/null @@ -1,19 +0,0 @@ -package libxvid - -import encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - -func NewEncoder() encoder2.EncoderContract { - parameters := map[string]encoder2.ParameterContract{} - getParams := func(parameters map[string]encoder2.ParameterContract) []string { - return []string{"-c:v", "libxvid"} - } - - return encoder2.NewEncoder("libxvid", parameters, getParams) -} - -func NewData() encoder2.EncoderDataContract { - title := "libxvid" - formats := []string{"avi"} - fileType := encoder2.FileType(encoder2.Video) - return encoder2.NewData(title, formats, fileType, NewEncoder) -} diff --git a/encoder/mjpeg/encoder.go b/encoder/mjpeg/encoder.go deleted file mode 100644 index be934e7..0000000 --- a/encoder/mjpeg/encoder.go +++ /dev/null @@ -1,19 +0,0 @@ -package mjpeg - -import encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - -func NewEncoder() encoder2.EncoderContract { - parameters := map[string]encoder2.ParameterContract{} - getParams := func(parameters map[string]encoder2.ParameterContract) []string { - return []string{"-c:v", "mjpeg"} - } - - return encoder2.NewEncoder("mjpeg", parameters, getParams) -} - -func NewData() encoder2.EncoderDataContract { - title := "mjpeg" - formats := []string{"jpg"} - fileType := encoder2.FileType(encoder2.Image) - return encoder2.NewData(title, formats, fileType, NewEncoder) -} diff --git a/encoder/mp2/encoder.go b/encoder/mp2/encoder.go deleted file mode 100644 index 3d1a98e..0000000 --- a/encoder/mp2/encoder.go +++ /dev/null @@ -1,19 +0,0 @@ -package mp2 - -import encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - -func NewEncoder() encoder2.EncoderContract { - parameters := map[string]encoder2.ParameterContract{} - getParams := func(parameters map[string]encoder2.ParameterContract) []string { - return []string{"-c:a", "mp2"} - } - - return encoder2.NewEncoder("mp2", parameters, getParams) -} - -func NewData() encoder2.EncoderDataContract { - title := "mp2" - formats := []string{"mp2"} - fileType := encoder2.FileType(encoder2.Audio) - return encoder2.NewData(title, formats, fileType, NewEncoder) -} diff --git a/encoder/mp2fixed/encoder.go b/encoder/mp2fixed/encoder.go deleted file mode 100644 index c235bee..0000000 --- a/encoder/mp2fixed/encoder.go +++ /dev/null @@ -1,19 +0,0 @@ -package mp2fixed - -import encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - -func NewEncoder() encoder2.EncoderContract { - parameters := map[string]encoder2.ParameterContract{} - getParams := func(parameters map[string]encoder2.ParameterContract) []string { - return []string{"-c:a", "mp2fixed"} - } - - return encoder2.NewEncoder("mp2fixed", parameters, getParams) -} - -func NewData() encoder2.EncoderDataContract { - title := "mp2fixed" - formats := []string{"mp2"} - fileType := encoder2.FileType(encoder2.Audio) - return encoder2.NewData(title, formats, fileType, NewEncoder) -} diff --git a/encoder/mpeg1video/encoder.go b/encoder/mpeg1video/encoder.go deleted file mode 100644 index 11c43e3..0000000 --- a/encoder/mpeg1video/encoder.go +++ /dev/null @@ -1,19 +0,0 @@ -package mpeg1video - -import encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - -func NewEncoder() encoder2.EncoderContract { - parameters := map[string]encoder2.ParameterContract{} - getParams := func(parameters map[string]encoder2.ParameterContract) []string { - return []string{"-c:v", "mpeg1video"} - } - - return encoder2.NewEncoder("mpeg1video", parameters, getParams) -} - -func NewData() encoder2.EncoderDataContract { - title := "mpeg1video" - formats := []string{"mpg", "mpeg"} - fileType := encoder2.FileType(encoder2.Video) - return encoder2.NewData(title, formats, fileType, NewEncoder) -} diff --git a/encoder/mpeg2video/encoder.go b/encoder/mpeg2video/encoder.go deleted file mode 100644 index 05192ef..0000000 --- a/encoder/mpeg2video/encoder.go +++ /dev/null @@ -1,19 +0,0 @@ -package mpeg2video - -import encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - -func NewEncoder() encoder2.EncoderContract { - parameters := map[string]encoder2.ParameterContract{} - getParams := func(parameters map[string]encoder2.ParameterContract) []string { - return []string{"-c:v", "mpeg2video"} - } - - return encoder2.NewEncoder("mpeg2video", parameters, getParams) -} - -func NewData() encoder2.EncoderDataContract { - title := "mpeg2video" - formats := []string{"mpg", "mpeg"} - fileType := encoder2.FileType(encoder2.Video) - return encoder2.NewData(title, formats, fileType, NewEncoder) -} diff --git a/encoder/mpeg4/encoder.go b/encoder/mpeg4/encoder.go deleted file mode 100644 index 5943f35..0000000 --- a/encoder/mpeg4/encoder.go +++ /dev/null @@ -1,19 +0,0 @@ -package mpeg4 - -import encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - -func NewEncoder() encoder2.EncoderContract { - parameters := map[string]encoder2.ParameterContract{} - getParams := func(parameters map[string]encoder2.ParameterContract) []string { - return []string{"-c:v", "mpeg4"} - } - - return encoder2.NewEncoder("mpeg4", parameters, getParams) -} - -func NewData() encoder2.EncoderDataContract { - title := "mpeg4" - formats := []string{"avi"} - fileType := encoder2.FileType(encoder2.Video) - return encoder2.NewData(title, formats, fileType, NewEncoder) -} diff --git a/encoder/msmpeg4/encoder.go b/encoder/msmpeg4/encoder.go deleted file mode 100644 index 246683d..0000000 --- a/encoder/msmpeg4/encoder.go +++ /dev/null @@ -1,19 +0,0 @@ -package msmpeg4 - -import encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - -func NewEncoder() encoder2.EncoderContract { - parameters := map[string]encoder2.ParameterContract{} - getParams := func(parameters map[string]encoder2.ParameterContract) []string { - return []string{"-c:v", "msmpeg4"} - } - - return encoder2.NewEncoder("msmpeg4", parameters, getParams) -} - -func NewData() encoder2.EncoderDataContract { - title := "msmpeg4" - formats := []string{"avi"} - fileType := encoder2.FileType(encoder2.Video) - return encoder2.NewData(title, formats, fileType, NewEncoder) -} diff --git a/encoder/msmpeg4v2/encoder.go b/encoder/msmpeg4v2/encoder.go deleted file mode 100644 index cc2cd5f..0000000 --- a/encoder/msmpeg4v2/encoder.go +++ /dev/null @@ -1,19 +0,0 @@ -package msmpeg4v2 - -import encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - -func NewEncoder() encoder2.EncoderContract { - parameters := map[string]encoder2.ParameterContract{} - getParams := func(parameters map[string]encoder2.ParameterContract) []string { - return []string{"-c:v", "msmpeg4v2"} - } - - return encoder2.NewEncoder("msmpeg4v2", parameters, getParams) -} - -func NewData() encoder2.EncoderDataContract { - title := "msmpeg4v2" - formats := []string{"avi"} - fileType := encoder2.FileType(encoder2.Video) - return encoder2.NewData(title, formats, fileType, NewEncoder) -} diff --git a/encoder/msvideo1/encoder.go b/encoder/msvideo1/encoder.go deleted file mode 100644 index e9bd448..0000000 --- a/encoder/msvideo1/encoder.go +++ /dev/null @@ -1,19 +0,0 @@ -package msvideo1 - -import encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - -func NewEncoder() encoder2.EncoderContract { - parameters := map[string]encoder2.ParameterContract{} - getParams := func(parameters map[string]encoder2.ParameterContract) []string { - return []string{"-c:v", "msvideo1"} - } - - return encoder2.NewEncoder("msvideo1", parameters, getParams) -} - -func NewData() encoder2.EncoderDataContract { - title := "msvideo1" - formats := []string{"avi"} - fileType := encoder2.FileType(encoder2.Video) - return encoder2.NewData(title, formats, fileType, NewEncoder) -} diff --git a/encoder/png/encoder.go b/encoder/png/encoder.go deleted file mode 100644 index 3166e58..0000000 --- a/encoder/png/encoder.go +++ /dev/null @@ -1,19 +0,0 @@ -package png - -import encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - -func NewEncoder() encoder2.EncoderContract { - parameters := map[string]encoder2.ParameterContract{} - getParams := func(parameters map[string]encoder2.ParameterContract) []string { - return []string{"-c:v", "png"} - } - - return encoder2.NewEncoder("png", parameters, getParams) -} - -func NewData() encoder2.EncoderDataContract { - title := "png" - formats := []string{"png"} - fileType := encoder2.FileType(encoder2.Image) - return encoder2.NewData(title, formats, fileType, NewEncoder) -} diff --git a/encoder/qtrle/encoder.go b/encoder/qtrle/encoder.go deleted file mode 100644 index befac18..0000000 --- a/encoder/qtrle/encoder.go +++ /dev/null @@ -1,19 +0,0 @@ -package qtrle - -import encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - -func NewEncoder() encoder2.EncoderContract { - parameters := map[string]encoder2.ParameterContract{} - getParams := func(parameters map[string]encoder2.ParameterContract) []string { - return []string{"-c:v", "qtrle"} - } - - return encoder2.NewEncoder("qtrle", parameters, getParams) -} - -func NewData() encoder2.EncoderDataContract { - title := "qtrle" - formats := []string{"mov"} - fileType := encoder2.FileType(encoder2.Video) - return encoder2.NewData(title, formats, fileType, NewEncoder) -} diff --git a/encoder/sgi/encoder.go b/encoder/sgi/encoder.go deleted file mode 100644 index b666b9b..0000000 --- a/encoder/sgi/encoder.go +++ /dev/null @@ -1,19 +0,0 @@ -package sgi - -import encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - -func NewEncoder() encoder2.EncoderContract { - parameters := map[string]encoder2.ParameterContract{} - getParams := func(parameters map[string]encoder2.ParameterContract) []string { - return []string{"-c:v", "sgi"} - } - - return encoder2.NewEncoder("sgi", parameters, getParams) -} - -func NewData() encoder2.EncoderDataContract { - title := "sgi" - formats := []string{"sgi"} - fileType := encoder2.FileType(encoder2.Image) - return encoder2.NewData(title, formats, fileType, NewEncoder) -} diff --git a/encoder/tiff/encoder.go b/encoder/tiff/encoder.go deleted file mode 100644 index 788d743..0000000 --- a/encoder/tiff/encoder.go +++ /dev/null @@ -1,19 +0,0 @@ -package tiff - -import encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - -func NewEncoder() encoder2.EncoderContract { - parameters := map[string]encoder2.ParameterContract{} - getParams := func(parameters map[string]encoder2.ParameterContract) []string { - return []string{"-c:v", "tiff"} - } - - return encoder2.NewEncoder("tiff", parameters, getParams) -} - -func NewData() encoder2.EncoderDataContract { - title := "tiff" - formats := []string{"tiff"} - fileType := encoder2.FileType(encoder2.Image) - return encoder2.NewData(title, formats, fileType, NewEncoder) -} diff --git a/encoder/wmav1/encoder.go b/encoder/wmav1/encoder.go deleted file mode 100644 index a2b9239..0000000 --- a/encoder/wmav1/encoder.go +++ /dev/null @@ -1,19 +0,0 @@ -package wmav1 - -import encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - -func NewEncoder() encoder2.EncoderContract { - parameters := map[string]encoder2.ParameterContract{} - getParams := func(parameters map[string]encoder2.ParameterContract) []string { - return []string{"-c:a", "wmav1"} - } - - return encoder2.NewEncoder("wmav1", parameters, getParams) -} - -func NewData() encoder2.EncoderDataContract { - title := "wmav1" - formats := []string{"wma"} - fileType := encoder2.FileType(encoder2.Audio) - return encoder2.NewData(title, formats, fileType, NewEncoder) -} diff --git a/encoder/wmav2/encoder.go b/encoder/wmav2/encoder.go deleted file mode 100644 index 66f490c..0000000 --- a/encoder/wmav2/encoder.go +++ /dev/null @@ -1,19 +0,0 @@ -package wmav2 - -import encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - -func NewEncoder() encoder2.EncoderContract { - parameters := map[string]encoder2.ParameterContract{} - getParams := func(parameters map[string]encoder2.ParameterContract) []string { - return []string{"-c:a", "wmav2"} - } - - return encoder2.NewEncoder("wmav2", parameters, getParams) -} - -func NewData() encoder2.EncoderDataContract { - title := "wmav2" - formats := []string{"wma"} - fileType := encoder2.FileType(encoder2.Audio) - return encoder2.NewData(title, formats, fileType, NewEncoder) -} diff --git a/encoder/wmv1/encoder.go b/encoder/wmv1/encoder.go deleted file mode 100644 index e5cf873..0000000 --- a/encoder/wmv1/encoder.go +++ /dev/null @@ -1,19 +0,0 @@ -package wmv1 - -import encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - -func NewEncoder() encoder2.EncoderContract { - parameters := map[string]encoder2.ParameterContract{} - getParams := func(parameters map[string]encoder2.ParameterContract) []string { - return []string{"-c:v", "wmv1"} - } - - return encoder2.NewEncoder("wmv1", parameters, getParams) -} - -func NewData() encoder2.EncoderDataContract { - title := "wmv1" - formats := []string{"wmv"} - fileType := encoder2.FileType(encoder2.Video) - return encoder2.NewData(title, formats, fileType, NewEncoder) -} diff --git a/encoder/wmv2/encoder.go b/encoder/wmv2/encoder.go deleted file mode 100644 index 4ca9797..0000000 --- a/encoder/wmv2/encoder.go +++ /dev/null @@ -1,19 +0,0 @@ -package wmv2 - -import encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - -func NewEncoder() encoder2.EncoderContract { - parameters := map[string]encoder2.ParameterContract{} - getParams := func(parameters map[string]encoder2.ParameterContract) []string { - return []string{"-c:v", "wmv2"} - } - - return encoder2.NewEncoder("wmv2", parameters, getParams) -} - -func NewData() encoder2.EncoderDataContract { - title := "wmv2" - formats := []string{"wmv"} - fileType := encoder2.FileType(encoder2.Video) - return encoder2.NewData(title, formats, fileType, NewEncoder) -} diff --git a/encoder/xbm/encoder.go b/encoder/xbm/encoder.go deleted file mode 100644 index d23f4a8..0000000 --- a/encoder/xbm/encoder.go +++ /dev/null @@ -1,19 +0,0 @@ -package xbm - -import encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - -func NewEncoder() encoder2.EncoderContract { - parameters := map[string]encoder2.ParameterContract{} - getParams := func(parameters map[string]encoder2.ParameterContract) []string { - return []string{"-c:v", "xbm"} - } - - return encoder2.NewEncoder("xbm", parameters, getParams) -} - -func NewData() encoder2.EncoderDataContract { - title := "xbm" - formats := []string{"xbm"} - fileType := encoder2.FileType(encoder2.Image) - return encoder2.NewData(title, formats, fileType, NewEncoder) -} diff --git a/error/view.go b/error/view.go deleted file mode 100644 index f2becd4..0000000 --- a/error/view.go +++ /dev/null @@ -1,39 +0,0 @@ -package error - -import ( - "fyne.io/fyne/v2/container" - "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" -) - -type ViewContract interface { - PanicError(err error) -} - -type View struct { - app kernel.AppContract -} - -func NewView(app kernel.AppContract) *View { - return &View{ - app: app, - } -} - -func (v View) PanicError(err error) { - messageHead := v.app.GetLocalizerService().GetMessage("error") - - v.app.GetWindow().SetContent(container.NewBorder( - container.NewVBox( - widget.NewLabel(messageHead), - widget.NewLabel(err.Error()), - ), - nil, - nil, - nil, - localizer.LanguageSelectionForm(v.app.GetLocalizerService(), func(lang kernel.Lang) { - v.PanicError(err) - }), - )) -} diff --git a/handler/convertor.go b/handler/convertor.go deleted file mode 100644 index 2bc81fe..0000000 --- a/handler/convertor.go +++ /dev/null @@ -1,178 +0,0 @@ -package handler - -import ( - "errors" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/convertor" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/convertor/view" - error2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/error" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/helper" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/setting" -) - -type ConvertorHandlerContract interface { - MainConvertor() - FfPathSelection() - GetFfmpegVersion() (string, error) - GetFfprobeVersion() (string, error) - GetFfplayVersion() (string, error) -} - -type ConvertorHandler struct { - app kernel.AppContract - convertorView convertor.ViewContract - errorView error2.ViewContract - convertorRepository convertor.RepositoryContract - settingDirectoryForSaving setting.DirectoryForSavingContract - itemsToConvertService kernel.ItemsToConvertContract -} - -func NewConvertorHandler( - app kernel.AppContract, - convertorView convertor.ViewContract, - errorView error2.ViewContract, - convertorRepository convertor.RepositoryContract, - settingDirectoryForSaving setting.DirectoryForSavingContract, - itemsToConvertService kernel.ItemsToConvertContract, -) *ConvertorHandler { - return &ConvertorHandler{ - app: app, - convertorView: convertorView, - errorView: errorView, - convertorRepository: convertorRepository, - settingDirectoryForSaving: settingDirectoryForSaving, - itemsToConvertService: itemsToConvertService, - } -} - -func (h ConvertorHandler) MainConvertor() { - if h.checkingFFPathUtilities() == true { - formats, err := h.app.GetConvertorService().GetSupportFormats() - if err != nil { - h.errorView.PanicError(err) - return - } - 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) -} - -func (h ConvertorHandler) FfPathSelection() { - ffmpeg := h.convertorRepository.GetPathFfmpeg() - ffprobe := h.convertorRepository.GetPathFfprobe() - ffplay := h.convertorRepository.GetPathFfplay() - h.convertorView.SelectFFPath(ffmpeg, ffprobe, ffplay, h.saveSettingFFPath, h.MainConvertor, h.downloadFFmpeg) -} - -func (h ConvertorHandler) GetFfmpegVersion() (string, error) { - return h.app.GetConvertorService().GetFFmpegVesrion() -} - -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.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 { - if h.checkingFFPath() == true { - return true - } - - pathsToFF := getPathsToFF() - for _, item := range pathsToFF { - ffmpegChecking, _ := h.app.GetConvertorService().ChangeFFmpegPath(item.FFmpeg) - if ffmpegChecking == false { - continue - } - ffprobeChecking, _ := h.app.GetConvertorService().ChangeFFprobePath(item.FFprobe) - 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, ffplayPath string) error { - ffmpegChecking, _ := h.app.GetConvertorService().ChangeFFmpegPath(ffmpegPath) - if ffmpegChecking == false { - errorText := h.app.GetLocalizerService().GetMessage("errorFFmpeg") - return errors.New(errorText) - } - - ffprobeChecking, _ := h.app.GetConvertorService().ChangeFFprobePath(ffprobePath) - if ffprobeChecking == false { - errorText := h.app.GetLocalizerService().GetMessage("errorFFprobe") - return errors.New(errorText) - } - - ffplayChecking, _ := h.app.GetConvertorService().ChangeFFplayPath(ffplayPath) - if ffplayChecking == false { - errorText := h.app.GetLocalizerService().GetMessage("errorFFplay") - return errors.New(errorText) - } - - _ = h.convertorRepository.SavePathFfmpeg(ffmpegPath) - _ = h.convertorRepository.SavePathFfprobe(ffprobePath) - _ = h.convertorRepository.SavePathFfplay(ffplayPath) - - h.MainConvertor() - - return nil -} - -func (h ConvertorHandler) checkingFFPath() bool { - _, err := h.app.GetConvertorService().GetFFmpegVesrion() - if err != nil { - return false - } - - _, err = h.app.GetConvertorService().GetFFprobeVersion() - if err != nil { - 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 deleted file mode 100644 index 76daa45..0000000 --- a/handler/convertor_anyos.go +++ /dev/null @@ -1,18 +0,0 @@ -//go:build !windows && !linux -// +build !windows,!linux - -package handler - -import ( - "fyne.io/fyne/v2/canvas" - "fyne.io/fyne/v2/widget" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" -) - -func getPathsToFF() []kernel.FFPathUtilities { - 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) { - return nil -} diff --git a/handler/convertor_linux.go b/handler/convertor_linux.go deleted file mode 100644 index 1c87a3c..0000000 --- a/handler/convertor_linux.go +++ /dev/null @@ -1,231 +0,0 @@ -//go:build linux -// +build linux - -package handler - -import ( - "archive/tar" - "errors" - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/canvas" - "fyne.io/fyne/v2/widget" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" - "github.com/ulikunitz/xz" - "io" - "net/http" - "os" - "path/filepath" -) - -func getPathsToFF() []kernel.FFPathUtilities { - 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) { - isDirectoryFFmpeg := isDirectory("ffmpeg") - if isDirectoryFFmpeg == false { - err = os.Mkdir("ffmpeg", 0777) - if err != nil { - return err - } - } - progressMessage.Text = h.app.GetLocalizerService().GetMessage("downloadRun") - fyne.Do(func() { - progressMessage.Refresh() - }) - err = downloadFile("ffmpeg/ffmpeg.tar.xz", "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz", progressBar) - if err != nil { - return err - } - - progressMessage.Text = h.app.GetLocalizerService().GetMessage("unzipRun") - fyne.Do(func() { - progressMessage.Refresh() - }) - err = unTarXz("ffmpeg/ffmpeg.tar.xz", "ffmpeg", progressBar) - if err != nil { - return err - } - _ = os.Remove("ffmpeg/ffmpeg.tar.xz") - - progressMessage.Text = h.app.GetLocalizerService().GetMessage("testFF") - fyne.Do(func() { - progressMessage.Refresh() - }) - - 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 - } - - progressMessage.Text = h.app.GetLocalizerService().GetMessage("completedQueue") - fyne.Do(func() { - progressMessage.Refresh() - }) - - return nil -} - -func downloadFile(filepath string, url string, progressBar *widget.ProgressBar) (err error) { - progressBar.Value = 0 - progressBar.Max = 100 - - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return err - } - resp, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - f, err := os.OpenFile(filepath, os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - return err - } - defer f.Close() - - buf := make([]byte, 32*1024) - var downloaded int64 - for { - n, err := resp.Body.Read(buf) - if err != nil { - if err == io.EOF { - break - } - return err - } - if n > 0 { - f.Write(buf[:n]) - downloaded += int64(n) - progressBar.Value = float64(downloaded) / float64(resp.ContentLength) * 100 - fyne.Do(func() { - progressBar.Refresh() - }) - } - } - return nil -} - -func unTarXz(fileTar string, directory string, progressBar *widget.ProgressBar) error { - progressBar.Value = 0 - progressBar.Max = 100 - - fyne.Do(func() { - progressBar.Refresh() - }) - - f, err := os.Open(fileTar) - if err != nil { - return err - } - defer f.Close() - - xzReader, err := xz.NewReader(f) - if err != nil { - return err - } - - tarReader := tar.NewReader(xzReader) - - totalFiles := 0 - for { - _, err := tarReader.Next() - if err == io.EOF { - break - } - if err != nil { - return err - } - totalFiles++ - } - - // Rewind back to the beginning of the file to re-process - _, err = f.Seek(0, 0) - if err != nil { - return err - } - - xzReader, err = xz.NewReader(f) - if err != nil { - return err - } - - tarReader = tar.NewReader(xzReader) - - // We count the number of files already unpacked - unpackedFiles := 0 - - for { - header, err := tarReader.Next() - if err == io.EOF { - break - } - if err != nil { - return err - } - - targetPath := filepath.Join(directory, header.Name) - switch header.Typeflag { - case tar.TypeDir: - err := os.MkdirAll(targetPath, 0755) - if err != nil { - return err - } - case tar.TypeReg: - outFile, err := os.Create(targetPath) - if err != nil { - return err - } - defer outFile.Close() - - _, err = io.Copy(outFile, tarReader) - - if err != nil { - return err - } - default: - return errors.New("unsupported file type") - } - - unpackedFiles++ - progressBar.Value = float64(unpackedFiles) / float64(totalFiles) * 100 - fyne.Do(func() { - progressBar.Refresh() - }) - } - - ffmpegPath := filepath.Join(directory, "ffmpeg-master-latest-linux64-gpl", "bin", "ffmpeg") - err = os.Chmod(ffmpegPath, 0755) - if err != nil { - return err - } - - ffprobePath := filepath.Join(directory, "ffmpeg-master-latest-linux64-gpl", "bin", "ffprobe") - err = os.Chmod(ffprobePath, 0755) - if err != nil { - 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 -} - -func isDirectory(path string) bool { - fileInfo, err := os.Stat(path) - if err != nil { - return false - } - - return fileInfo.IsDir() -} diff --git a/handler/convertor_windows.go b/handler/convertor_windows.go deleted file mode 100644 index 206aa57..0000000 --- a/handler/convertor_windows.go +++ /dev/null @@ -1,185 +0,0 @@ -//go:build windows -// +build windows - -package handler - -import ( - "archive/zip" - "errors" - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/canvas" - "fyne.io/fyne/v2/widget" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" - "io" - "net/http" - "os" - "path/filepath" - "strings" -) - -func getPathsToFF() []kernel.FFPathUtilities { - 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) { - isDirectoryFFmpeg := isDirectory("ffmpeg") - if isDirectoryFFmpeg == false { - err = os.Mkdir("ffmpeg", 0777) - if err != nil { - return err - } - } - progressMessage.Text = h.app.GetLocalizerService().GetMessage("downloadRun") - fyne.Do(func() { - progressMessage.Refresh() - }) - err = downloadFile("ffmpeg/ffmpeg.zip", "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip", progressBar) - if err != nil { - return err - } - - progressMessage.Text = h.app.GetLocalizerService().GetMessage("unzipRun") - fyne.Do(func() { - progressMessage.Refresh() - }) - err = unZip("ffmpeg/ffmpeg.zip", "ffmpeg", progressBar) - if err != nil { - return err - } - _ = os.Remove("ffmpeg/ffmpeg.zip") - - progressMessage.Text = h.app.GetLocalizerService().GetMessage("testFF") - 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", - "ffmpeg/ffmpeg-master-latest-win64-gpl/bin/ffplay.exe", - ) - if err != nil { - return err - } - - progressMessage.Text = h.app.GetLocalizerService().GetMessage("completedQueue") - fyne.Do(func() { - progressMessage.Refresh() - }) - - return nil -} - -func downloadFile(filepath string, url string, progressBar *widget.ProgressBar) (err error) { - progressBar.Value = 0 - progressBar.Max = 100 - - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return err - } - resp, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - f, err := os.OpenFile(filepath, os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - return err - } - defer f.Close() - - buf := make([]byte, 32*1024) - var downloaded int64 - for { - n, err := resp.Body.Read(buf) - if err != nil { - if err == io.EOF { - break - } - return err - } - if n > 0 { - f.Write(buf[:n]) - downloaded += int64(n) - progressBar.Value = float64(downloaded) / float64(resp.ContentLength) * 100 - fyne.Do(func() { - progressBar.Refresh() - }) - } - } - return nil -} - -func unZip(fileZip string, directory string, progressBar *widget.ProgressBar) error { - progressBar.Value = 0 - progressBar.Max = 100 - - fyne.Do(func() { - progressBar.Refresh() - }) - - archive, err := zip.OpenReader(fileZip) - if err != nil { - return err - } - defer archive.Close() - - totalBytes := int64(0) - for _, f := range archive.File { - totalBytes += int64(f.UncompressedSize64) - } - - unpackedBytes := int64(0) - - for _, f := range archive.File { - filePath := filepath.Join(directory, f.Name) - - if !strings.HasPrefix(filePath, filepath.Clean(directory)+string(os.PathSeparator)) { - return errors.New("invalid file path") - } - if f.FileInfo().IsDir() { - os.MkdirAll(filePath, os.ModePerm) - continue - } - - if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil { - return err - } - - dstFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) - if err != nil { - return err - } - - fileInArchive, err := f.Open() - if err != nil { - return err - } - - bytesRead, err := io.Copy(dstFile, fileInArchive) - if err != nil { - return err - } - - unpackedBytes += bytesRead - progressBar.Value = float64(unpackedBytes) / float64(totalBytes) * 100 - fyne.Do(func() { - progressBar.Refresh() - }) - - dstFile.Close() - fileInArchive.Close() - } - - return nil -} - -func isDirectory(path string) bool { - fileInfo, err := os.Stat(path) - if err != nil { - return false - } - - return fileInfo.IsDir() -} diff --git a/handler/main.go b/handler/main.go deleted file mode 100644 index 09d4fa8..0000000 --- a/handler/main.go +++ /dev/null @@ -1,32 +0,0 @@ -package handler - -import ( - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" -) - -type MainHandler struct { - app kernel.AppContract - convertorHandler ConvertorHandlerContract - menuHandler MenuHandlerContract -} - -func NewMainHandler( - app kernel.AppContract, - convertorHandler ConvertorHandlerContract, - menuHandler MenuHandlerContract, -) *MainHandler { - return &MainHandler{ - app: app, - convertorHandler: convertorHandler, - menuHandler: menuHandler, - } -} - -func (h MainHandler) Start() { - if h.app.GetLocalizerService().IsStartWithLanguageSelection() { - h.menuHandler.LanguageSelection() - return - } - - h.convertorHandler.MainConvertor() -} diff --git a/handler/menu.go b/handler/menu.go deleted file mode 100644 index 932a0ed..0000000 --- a/handler/menu.go +++ /dev/null @@ -1,143 +0,0 @@ -package handler - -import ( - "fyne.io/fyne/v2" - "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" -) - -type MenuHandlerContract interface { - GetMainMenu() *fyne.MainMenu - LanguageSelection() -} - -type MenuHandler struct { - app kernel.AppContract - convertorHandler ConvertorHandlerContract - menuView menu.ViewContract - menuViewSetting menu.ViewSettingContract - localizerView localizer.ViewContract - themeService theme.ThemeContract -} - -func NewMenuHandler( - app kernel.AppContract, - convertorHandler ConvertorHandlerContract, - menuView menu.ViewContract, - menuViewSetting menu.ViewSettingContract, - localizerView localizer.ViewContract, - themeService theme.ThemeContract, -) *MenuHandler { - return &MenuHandler{ - app: app, - convertorHandler: convertorHandler, - menuView: menuView, - menuViewSetting: menuViewSetting, - localizerView: localizerView, - themeService: themeService, - } -} - -func (h MenuHandler) GetMainMenu() *fyne.MainMenu { - settings := h.getMenuSettings() - help := h.getMenuHelp() - - return fyne.NewMainMenu(settings, help) -} - -func (h MenuHandler) getMenuSettings() *fyne.Menu { - quit := fyne.NewMenuItem(h.app.GetLocalizerService().GetMessage("exit"), nil) - quit.IsQuit = true - h.app.GetLocalizerService().AddChangeCallback("exit", func(text string) { - quit.Label = text - }) - - settingsSelection := fyne.NewMenuItem(h.app.GetLocalizerService().GetMessage("settings"), h.settingsSelection) - h.app.GetLocalizerService().AddChangeCallback("settings", func(text string) { - settingsSelection.Label = text - }) - - ffPathSelection := fyne.NewMenuItem(h.app.GetLocalizerService().GetMessage("changeFFPath"), h.convertorHandler.FfPathSelection) - h.app.GetLocalizerService().AddChangeCallback("changeFFPath", func(text string) { - ffPathSelection.Label = text - }) - - settings := fyne.NewMenu(h.app.GetLocalizerService().GetMessage("settings"), settingsSelection, ffPathSelection, quit) - h.app.GetLocalizerService().AddChangeCallback("settings", func(text string) { - settings.Label = text - settings.Refresh() - }) - - return settings -} - -func (h MenuHandler) getMenuHelp() *fyne.Menu { - about := fyne.NewMenuItem(h.app.GetLocalizerService().GetMessage("about"), h.openAbout) - h.app.GetLocalizerService().AddChangeCallback("about", func(text string) { - about.Label = text - }) - - gratitude := fyne.NewMenuItem(h.app.GetLocalizerService().GetMessage("gratitude"), h.menuView.Gratitude) - h.app.GetLocalizerService().AddChangeCallback("gratitude", func(text string) { - gratitude.Label = text - }) - - helpFFplay := fyne.NewMenuItem(h.app.GetLocalizerService().GetMessage("helpFFplay"), h.menuView.HelpFFplay) - h.app.GetLocalizerService().AddChangeCallback("helpFFplay", func(text string) { - helpFFplay.Label = text - }) - - help := fyne.NewMenu(h.app.GetLocalizerService().GetMessage("help"), helpFFplay, about, gratitude) - h.app.GetLocalizerService().AddChangeCallback("help", func(text string) { - help.Label = text - help.Refresh() - }) - - return help -} - -func (h MenuHandler) openAbout() { - ffmpeg, err := h.convertorHandler.GetFfmpegVersion() - if err != nil { - ffmpeg = h.app.GetLocalizerService().GetMessage("errorFFmpegVersion") - } - ffprobe, err := h.convertorHandler.GetFfprobeVersion() - if err != nil { - ffprobe = h.app.GetLocalizerService().GetMessage("errorFFprobeVersion") - } - ffplay, err := h.convertorHandler.GetFfplayVersion() - if err != nil { - ffplay = h.app.GetLocalizerService().GetMessage("errorFFplayVersion") - } - - h.menuView.About(ffmpeg, ffprobe, ffplay) -} - -func (h MenuHandler) LanguageSelection() { - h.localizerView.LanguageSelection(func(lang kernel.Lang) { - h.convertorHandler.MainConvertor() - }) -} - -func (h MenuHandler) settingsSelection() { - save := func(setting *menu.SettingForm) error { - err := h.app.GetLocalizerService().SetCurrentLanguage(setting.Language, true) - 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/helper/helper.go b/helper/helper.go deleted file mode 100644 index 682704c..0000000 --- a/helper/helper.go +++ /dev/null @@ -1,11 +0,0 @@ -package helper - -import ( - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/dialog" -) - -func FileDialogResize(fileDialog *dialog.FileDialog, w fyne.Window) { - contentSize := w.Content().Size() - fileDialog.Resize(fyne.Size{Width: contentSize.Width - 50, Height: contentSize.Height - 50}) -} diff --git a/helper/path_separator.go b/helper/path_separator.go deleted file mode 100644 index 905b935..0000000 --- a/helper/path_separator.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build !windows -// +build !windows - -package helper - -func PathSeparator() string { - return "/" -} diff --git a/helper/path_separator_window.go b/helper/path_separator_window.go deleted file mode 100644 index 433e550..0000000 --- a/helper/path_separator_window.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build windows -// +build windows - -package helper - -func PathSeparator() string { - return "\\" -} diff --git a/helper/prepare_background_command.go b/helper/prepare_background_command.go deleted file mode 100644 index f8aab96..0000000 --- a/helper/prepare_background_command.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build !windows -// +build !windows - -package helper - -import ( - "os/exec" -) - -func PrepareBackgroundCommand(cmd *exec.Cmd) { - -} diff --git a/helper/prepare_background_command_windows.go b/helper/prepare_background_command_windows.go deleted file mode 100644 index 3e3ebbc..0000000 --- a/helper/prepare_background_command_windows.go +++ /dev/null @@ -1,13 +0,0 @@ -//go:build windows -// +build windows - -package helper - -import ( - "os/exec" - "syscall" -) - -func PrepareBackgroundCommand(cmd *exec.Cmd) { - cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} -} diff --git a/kernel/app.go b/kernel/app.go deleted file mode 100644 index 242ea8c..0000000 --- a/kernel/app.go +++ /dev/null @@ -1,132 +0,0 @@ -package kernel - -import ( - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/app" - "time" -) - -type AppContract interface { - GetAppFyne() fyne.App - GetWindow() WindowContract - GetQueue() QueueListContract - GetLocalizerService() LocalizerContract - GetConvertorService() ConvertorContract - GetFFplayService() FFplayContract - AfterClosing() - RunConvertor() -} - -type App struct { - AppFyne fyne.App - Window WindowContract - Queue QueueListContract - - localizerService LocalizerContract - convertorService ConvertorContract - blockProgressbarService BlockProgressbarContract - ffplayService FFplayContract -} - -func NewApp( - metadata *fyne.AppMetadata, - queue QueueListContract, - ffplayService FFplayContract, - convertorService ConvertorContract, -) *App { - app.SetMetadata(*metadata) - a := app.New() - - localizerService, err := newLocalizer(a) - if err != nil { - panicErrorLang(a, err) - return nil - } - - 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, rightTabsService)), - Queue: queue, - - localizerService: localizerService, - convertorService: convertorService, - blockProgressbarService: blockProgressbarService, - ffplayService: ffplayService, - } -} - -func (a App) GetAppFyne() fyne.App { - return a.AppFyne -} - -func (a App) GetQueue() QueueListContract { - return a.Queue -} - -func (a App) GetWindow() WindowContract { - return a.Window -} - -func (a App) GetLocalizerService() LocalizerContract { - return a.localizerService -} - -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() - } -} - -func (a App) RunConvertor() { - go func() { - for { - time.Sleep(time.Millisecond * 3000) - queueId, queue := a.Queue.Next() - if queue == nil { - continue - } - 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.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 deleted file mode 100644 index 179cff3..0000000 --- a/kernel/convertor.go +++ /dev/null @@ -1,290 +0,0 @@ -package kernel - -import ( - "bufio" - "errors" - encoder2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/helper" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel/encoder" - "io" - "os/exec" - "regexp" - "strconv" - "strings" - "unicode" -) - -type File struct { - Path string - Name string - Ext string -} - -type ConvertSetting struct { - VideoFileInput File - VideoFileOut File - OverwriteOutputFiles bool - Encoder encoder2.EncoderContract -} - -type ConvertorContract interface { - RunConvert(setting ConvertSetting, progress ProgressContract) error - 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) -} - -type ProgressContract interface { - GetProtocole() string - Run(stdOut io.ReadCloser, stdErr io.ReadCloser) error -} - -type FFPathUtilities struct { - FFmpeg string - FFprobe string - FFplay string -} - -type runningProcesses struct { - items map[int]*exec.Cmd - numberOfStarts int -} - -type Convertor struct { - ffPathUtilities *FFPathUtilities - runningProcesses runningProcesses -} - -type ConvertData struct { - totalDuration float64 -} - -func NewService(ffPathUtilities *FFPathUtilities) *Convertor { - return &Convertor{ - ffPathUtilities: ffPathUtilities, - runningProcesses: runningProcesses{items: map[int]*exec.Cmd{}, numberOfStarts: 0}, - } -} - -func (s Convertor) RunConvert(setting ConvertSetting, progress ProgressContract) error { - overwriteOutputFiles := "-n" - if setting.OverwriteOutputFiles == true { - overwriteOutputFiles = "-y" - } - args := []string{overwriteOutputFiles, "-i", setting.VideoFileInput.Path} - args = append(args, setting.Encoder.GetParams()...) - args = append(args, "-progress", progress.GetProtocole(), setting.VideoFileOut.Path) - cmd := exec.Command(s.ffPathUtilities.FFmpeg, args...) - helper.PrepareBackgroundCommand(cmd) - - stdOut, err := cmd.StdoutPipe() - if err != nil { - return err - } - stdErr, err := cmd.StderrPipe() - if err != nil { - return err - } - - err = cmd.Start() - if err != nil { - return err - } - index := s.runningProcesses.numberOfStarts - s.runningProcesses.numberOfStarts++ - s.runningProcesses.items[index] = cmd - - errProgress := progress.Run(stdOut, stdErr) - - err = cmd.Wait() - delete(s.runningProcesses.items, index) - if errProgress != nil { - return errProgress - } - if err != nil { - return err - } - - return nil -} - -func (s Convertor) GetTotalDuration(file *File) (duration float64, err error) { - args := []string{"-v", "error", "-select_streams", "v:0", "-count_packets", "-show_entries", "stream=nb_read_packets", "-of", "csv=p=0", file.Path} - cmd := exec.Command(s.ffPathUtilities.FFprobe, args...) - helper.PrepareBackgroundCommand(cmd) - out, err := cmd.CombinedOutput() - if err != nil { - errString := strings.TrimSpace(string(out)) - if len(errString) > 1 { - return 0, errors.New(errString) - } - return 0, err - } - frames := strings.TrimSpace(string(out)) - if len(frames) == 0 { - return s.getAlternativeTotalDuration(file) - } - - duration, err = strconv.ParseFloat(frames, 64) - if err != nil { - // fix .mts duration - return strconv.ParseFloat(getFirstDigits(frames), 64) - } - return duration, err -} - -func (s Convertor) getAlternativeTotalDuration(file *File) (duration float64, err error) { - args := []string{"-v", "error", "-select_streams", "a:0", "-count_packets", "-show_entries", "stream=nb_read_packets", "-of", "csv=p=0", file.Path} - cmd := exec.Command(s.ffPathUtilities.FFprobe, args...) - helper.PrepareBackgroundCommand(cmd) - out, err := cmd.CombinedOutput() - if err != nil { - errString := strings.TrimSpace(string(out)) - if len(errString) > 1 { - return 0, errors.New(errString) - } - return 0, err - } - frames := strings.TrimSpace(string(out)) - if len(frames) == 0 { - return 0, errors.New("error getting number of frames") - } - return strconv.ParseFloat(frames, 64) -} - -func (s Convertor) GetFFmpegVesrion() (string, error) { - cmd := exec.Command(s.ffPathUtilities.FFmpeg, "-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) GetFFprobeVersion() (string, error) { - cmd := exec.Command(s.ffPathUtilities.FFprobe, "-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) 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) - out, err := cmd.CombinedOutput() - if err != nil { - return false, err - } - if strings.Contains(strings.TrimSpace(string(out)), "ffmpeg") == false { - return false, nil - } - s.ffPathUtilities.FFmpeg = path - return true, nil -} - -func (s Convertor) ChangeFFprobePath(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)), "ffprobe") == false { - return false, nil - } - s.ffPathUtilities.FFprobe = path - 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") - helper.PrepareBackgroundCommand(cmd) - - stdOut, err := cmd.StdoutPipe() - if err != nil { - return formats, err - } - - err = cmd.Start() - if err != nil { - return formats, err - } - - scannerErr := bufio.NewReader(stdOut) - for { - line, _, err := scannerErr.ReadLine() - if err != nil { - if err == io.EOF { - break - } - continue - } - text := strings.Split(strings.TrimSpace(string(line)), " ") - encoderType := string(text[0][0]) - if len(text) < 2 || (encoderType != "V" && encoderType != "A") { - continue - } - formats.NewEncoder(text[1]) - } - - err = cmd.Wait() - if err != nil { - return formats, err - } - - return formats, nil -} - -func (s Convertor) GetRunningProcesses() map[int]*exec.Cmd { - return s.runningProcesses.items -} - -func getFirstDigits(s string) string { - result := "" - for _, r := range s { - if unicode.IsDigit(r) { - result += string(r) - } else { - break - } - } - return result -} diff --git a/kernel/encoder/encoder.go b/kernel/encoder/encoder.go deleted file mode 100644 index db4e25f..0000000 --- a/kernel/encoder/encoder.go +++ /dev/null @@ -1,84 +0,0 @@ -package encoder - -import ( - "errors" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" -) - -type ConvertorFormatContract interface { - GetTitle() string - AddEncoder(encoder encoder.EncoderDataContract) - GetFileType() encoder.FileTypeContract - GetEncoders() map[int]encoder.EncoderDataContract -} - -type ConvertorFormat struct { - title string - fileType encoder.FileTypeContract - encoders map[int]encoder.EncoderDataContract -} - -func NewConvertorFormat(title string, fileType encoder.FileTypeContract) *ConvertorFormat { - return &ConvertorFormat{ - title: title, - fileType: fileType, - encoders: map[int]encoder.EncoderDataContract{}, - } -} - -func (f ConvertorFormat) GetTitle() string { - return f.title -} - -func (f ConvertorFormat) AddEncoder(encoder encoder.EncoderDataContract) { - f.encoders[len(f.encoders)] = encoder -} - -func (f ConvertorFormat) GetEncoders() map[int]encoder.EncoderDataContract { - return f.encoders -} - -func (f ConvertorFormat) GetFileType() encoder.FileTypeContract { - return f.fileType -} - -type ConvertorFormatsContract interface { - NewEncoder(encoderName string) bool - GetFormats() map[string]ConvertorFormatContract - GetFormat(format string) (ConvertorFormatContract, error) -} - -type ConvertorFormats struct { - formats map[string]ConvertorFormatContract -} - -func NewConvertorFormats() *ConvertorFormats { - return &ConvertorFormats{ - formats: map[string]ConvertorFormatContract{}, - } -} - -func (f ConvertorFormats) NewEncoder(encoderName string) bool { - if supportEncoders[encoderName] == nil { - return false - } - data := supportEncoders[encoderName]() - for _, format := range data.GetFormats() { - if f.formats[format] == nil { - f.formats[format] = NewConvertorFormat(format, data.GetFileType()) - } - f.formats[format].AddEncoder(data) - } - return true -} - -func (f ConvertorFormats) GetFormats() map[string]ConvertorFormatContract { - return f.formats -} - -func (f ConvertorFormats) GetFormat(format string) (ConvertorFormatContract, error) { - if f.formats[format] == nil { - return ConvertorFormat{}, errors.New("not found ConvertorFormat") - } - return f.formats[format], nil -} diff --git a/kernel/encoder/encoders.go b/kernel/encoder/encoders.go deleted file mode 100644 index d9330f4..0000000 --- a/kernel/encoder/encoders.go +++ /dev/null @@ -1,74 +0,0 @@ -package encoder - -import ( - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/apng" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/bmp" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/flv" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/gif" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/h264_nvenc" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/libmp3lame" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/libshine" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/libtwolame" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/libvpx" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/libvpx_vp9" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/libwebp" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/libwebp_anim" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/libx264" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/libx265" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/libxvid" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/mjpeg" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/mp2" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/mp2fixed" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/mpeg1video" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/mpeg2video" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/mpeg4" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/msmpeg4" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/msmpeg4v2" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/msvideo1" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/png" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/qtrle" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/sgi" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/tiff" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/wmav1" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/wmav2" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/wmv1" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/wmv2" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/encoder/xbm" -) - -var supportEncoders = map[string]func() encoder.EncoderDataContract{ - "libx264": libx264.NewData, - "h264_nvenc": h264_nvenc.NewData, - "libx265": libx265.NewData, - "png": png.NewData, - "gif": gif.NewData, - "flv": flv.NewData, - "apng": apng.NewData, - "bmp": bmp.NewData, - "mjpeg": mjpeg.NewData, - "mpeg1video": mpeg1video.NewData, - "mpeg2video": mpeg2video.NewData, - "mpeg4": mpeg4.NewData, - "libxvid": libxvid.NewData, - "msmpeg4v2": msmpeg4v2.NewData, - "msmpeg4": msmpeg4.NewData, - "msvideo1": msvideo1.NewData, - "qtrle": qtrle.NewData, - "tiff": tiff.NewData, - "sgi": sgi.NewData, - "libvpx": libvpx.NewData, - "libvpx-vp9": libvpx_vp9.NewData, - "libwebp_anim": libwebp_anim.NewData, - "libwebp": libwebp.NewData, - "wmv1": wmv1.NewData, - "wmv2": wmv2.NewData, - "xbm": xbm.NewData, - "mp2": mp2.NewData, - "mp2fixed": mp2fixed.NewData, - "libtwolame": libtwolame.NewData, - "libmp3lame": libmp3lame.NewData, - "libshine": libshine.NewData, - "wmav1": wmav1.NewData, - "wmav2": wmav2.NewData, -} diff --git a/kernel/error.go b/kernel/error.go deleted file mode 100644 index cd26e13..0000000 --- a/kernel/error.go +++ /dev/null @@ -1,17 +0,0 @@ -package kernel - -import ( - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/widget" -) - -func panicErrorLang(a fyne.App, err error) { - window := a.NewWindow("GUI for FFmpeg") - window.SetContent(container.NewVBox( - widget.NewLabel("Произошла ошибка!"), - widget.NewLabel("произошла ошибка при получении языковых переводах. \n\r"+err.Error()), - )) - window.ShowAndRun() - panic(err.Error()) -} diff --git a/kernel/ffplay.go b/kernel/ffplay.go deleted file mode 100644 index 0b5538f..0000000 --- a/kernel/ffplay.go +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index 093540c..0000000 --- a/kernel/items_to_convert.go +++ /dev/null @@ -1,152 +0,0 @@ -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" -) - -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("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("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 deleted file mode 100644 index 7953967..0000000 --- a/kernel/layout.go +++ /dev/null @@ -1,481 +0,0 @@ -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" - "image/color" - "strconv" - "strings" -) - -type LayoutContract interface { - SetContent(content fyne.CanvasObject) *fyne.Container - 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, rightTabsService RightTabsContract) *Layout { - layout := container.NewAdaptiveGrid(2, widget.NewLabel(""), queueLayoutObject.GetCanvasObject()) - - return &Layout{ - layout: layout, - queueLayoutObject: queueLayoutObject, - localizerService: localizerService, - rightTabsService: rightTabsService, - } -} - -func (l Layout) SetContent(content fyne.CanvasObject) *fyne.Container { - l.layout.Objects[0] = content - return l.layout -} - -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 - ChangeQueueStatus(queueId int, queue *Queue) -} - -type QueueLayoutObject struct { - QueueListContract QueueListContract - - queue QueueListContract - container *fyne.Container - containerItems *fyne.Container - items map[int]QueueLayoutItem - localizerService LocalizerContract - queueStatisticsFormat *queueStatisticsFormat - ffplayService FFplayContract -} - -type QueueLayoutItem struct { - CanvasObject fyne.CanvasObject - BlockMessageError *container.Scroll - StatusMessage *canvas.Text - MessageError *canvas.Text - buttonPlay *widget.Button - - status *StatusContract -} - -func NewQueueLayoutObject(queue QueueListContract, localizerService LocalizerContract, ffplayService FFplayContract, rightTabsService RightTabsContract, blockProgressbar *fyne.Container) *QueueLayoutObject { - title := widget.NewLabel(localizerService.GetMessage("queue")) - title.TextStyle.Bold = true - - localizerService.AddChangeCallback("queue", func(text string) { - title.Text = text - title.Refresh() - }) - - 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.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) - - return queueLayoutObject -} - -func (o QueueLayoutObject) GetCanvasObject() fyne.CanvasObject { - return o.container -} - -func (o QueueLayoutObject) Add(id int, queue *Queue) { - 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)), - container.NewHBox( - buttonPlay, - statusMessage, - ), - blockMessageError, - container.NewPadded(), - canvas.NewLine(theme.Color(theme.ColorNameFocus)), - container.NewPadded(), - ) - - o.queueStatisticsFormat.addQueue() - if o.queueStatisticsFormat.isChecked(queue.Status) == false { - content.Hide() - } - - o.items[id] = QueueLayoutItem{ - CanvasObject: content, - StatusMessage: statusMessage, - BlockMessageError: blockMessageError, - MessageError: messageError, - buttonPlay: buttonPlay, - status: &queue.Status, - } - o.containerItems.Add(content) -} - -func (o QueueLayoutObject) Remove(id int) { - if item, ok := o.items[id]; ok { - o.container.Remove(item.CanvasObject) - o.queueStatisticsFormat.removeQueue(*item.status) - o.items[id] = QueueLayoutItem{} - } -} - -func (o QueueLayoutObject) ChangeQueueStatus(queueId int, queue *Queue) { - if item, ok := o.items[queueId]; ok { - statusColor := o.getStatusColor(queue.Status) - item.StatusMessage.Text = o.getStatusTitle(queue.Status) - item.StatusMessage.Color = statusColor - fyne.Do(func() { - item.StatusMessage.Refresh() - }) - if queue.Error != nil { - 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 { - item.CanvasObject.Show() - } - o.queueStatisticsFormat.changeQueue(queue.Status) - } -} - -func (o QueueLayoutObject) getStatusColor(status StatusContract) color.Color { - if status == StatusType(Error) { - return theme.Color(theme.ColorNameError) - } - - if status == StatusType(Completed) { - return color.RGBA{R: 49, G: 127, B: 114, A: 255} - } - - return theme.Color(theme.ColorNamePrimary) -} - -func (o QueueLayoutObject) getStatusTitle(status StatusContract) string { - return o.localizerService.GetMessage(status.Name() + "Queue") -} - -type queueStatistics struct { - widget *widget.Check - title string - count *int64 -} -type queueStatisticsFormat struct { - waiting *queueStatistics - inProgress *queueStatistics - completed *queueStatistics - error *queueStatistics - total *queueStatistics -} - -func newQueueStatisticsFormat(localizerService LocalizerContract, queueItems *map[int]QueueLayoutItem) *queueStatisticsFormat { - checkWaiting := newQueueStatistics("waitingQueue", localizerService) - checkInProgress := newQueueStatistics("inProgressQueue", localizerService) - checkCompleted := newQueueStatistics("completedQueue", localizerService) - checkError := newQueueStatistics("errorQueue", localizerService) - checkTotal := newQueueStatistics("total", localizerService) - - queueStatisticsFormat := &queueStatisticsFormat{ - waiting: checkWaiting, - inProgress: checkInProgress, - completed: checkCompleted, - error: checkError, - total: checkTotal, - } - - checkTotal.widget.OnChanged = func(b bool) { - if b == true { - queueStatisticsFormat.allCheckboxChecked() - } else { - queueStatisticsFormat.allUnCheckboxChecked() - } - queueStatisticsFormat.redrawingQueueItems(queueItems) - } - - queueStatisticsFormat.waiting.widget.OnChanged = func(b bool) { - if b == true { - queueStatisticsFormat.checkboxChecked() - } else { - queueStatisticsFormat.unCheckboxChecked() - } - queueStatisticsFormat.redrawingQueueItems(queueItems) - } - - queueStatisticsFormat.inProgress.widget.OnChanged = func(b bool) { - if b == true { - queueStatisticsFormat.checkboxChecked() - } else { - queueStatisticsFormat.unCheckboxChecked() - } - queueStatisticsFormat.redrawingQueueItems(queueItems) - } - - queueStatisticsFormat.completed.widget.OnChanged = func(b bool) { - if b == true { - queueStatisticsFormat.checkboxChecked() - } else { - queueStatisticsFormat.unCheckboxChecked() - } - queueStatisticsFormat.redrawingQueueItems(queueItems) - } - - queueStatisticsFormat.error.widget.OnChanged = func(b bool) { - if b == true { - queueStatisticsFormat.checkboxChecked() - } else { - queueStatisticsFormat.unCheckboxChecked() - } - queueStatisticsFormat.redrawingQueueItems(queueItems) - } - - return queueStatisticsFormat -} - -func (f queueStatisticsFormat) redrawingQueueItems(queueItems *map[int]QueueLayoutItem) { - for _, item := range *queueItems { - if f.isChecked(*item.status) == true && item.CanvasObject.Visible() == false { - item.CanvasObject.Show() - continue - } - if f.isChecked(*item.status) == false && item.CanvasObject.Visible() == true { - item.CanvasObject.Hide() - } - } -} - -func (f queueStatisticsFormat) isChecked(status StatusContract) bool { - if status == StatusType(InProgress) { - return f.inProgress.widget.Checked - } - if status == StatusType(Completed) { - return f.completed.widget.Checked - } - if status == StatusType(Error) { - return f.error.widget.Checked - } - if status == StatusType(Waiting) { - return f.waiting.widget.Checked - } - - return true -} - -func (f queueStatisticsFormat) addQueue() { - f.waiting.add() - f.total.add() -} - -func (f queueStatisticsFormat) changeQueue(status StatusContract) { - if status == StatusType(InProgress) { - f.waiting.remove() - f.inProgress.add() - return - } - - if status == StatusType(Completed) { - f.inProgress.remove() - f.completed.add() - return - } - - if status == StatusType(Error) { - f.inProgress.remove() - f.error.add() - return - } -} - -func (f queueStatisticsFormat) removeQueue(status StatusContract) { - f.total.remove() - - if status == StatusType(Completed) { - f.completed.remove() - return - } - - if status == StatusType(Error) { - f.error.remove() - return - } - - if status == StatusType(InProgress) { - f.inProgress.remove() - return - } - - if status == StatusType(Waiting) { - f.waiting.remove() - return - } -} - -func (f queueStatisticsFormat) checkboxChecked() { - if f.total.widget.Checked == true { - return - } - - if f.waiting.widget.Checked == false { - return - } - - if f.inProgress.widget.Checked == false { - return - } - - if f.completed.widget.Checked == false { - return - } - - if f.error.widget.Checked == false { - return - } - - f.total.widget.Checked = true - f.total.widget.Refresh() -} - -func (f queueStatisticsFormat) unCheckboxChecked() { - if f.total.widget.Checked == false { - return - } - - f.total.widget.Checked = false - f.total.widget.Refresh() -} - -func (f queueStatisticsFormat) allCheckboxChecked() { - f.waiting.widget.Checked = true - f.waiting.widget.Refresh() - f.inProgress.widget.Checked = true - f.inProgress.widget.Refresh() - f.completed.widget.Checked = true - f.completed.widget.Refresh() - f.error.widget.Checked = true - f.error.widget.Refresh() -} - -func (f queueStatisticsFormat) allUnCheckboxChecked() { - f.waiting.widget.Checked = false - f.waiting.widget.Refresh() - f.inProgress.widget.Checked = false - f.inProgress.widget.Refresh() - f.completed.widget.Checked = false - f.completed.widget.Refresh() - f.error.widget.Checked = false - f.error.widget.Refresh() -} - -func newQueueStatistics(messaigeID string, localizerService LocalizerContract) *queueStatistics { - checkbox := widget.NewCheck("", nil) - checkbox.Checked = true - - count := int64(0) - - title := localizerService.GetMessage(messaigeID) - queueStatistics := &queueStatistics{ - widget: checkbox, - title: strings.ToLower(title), - count: &count, - } - - queueStatistics.formatText(false) - - localizerService.AddChangeCallback(messaigeID, func(text string) { - queueStatistics.title = strings.ToLower(text) - queueStatistics.formatText(true) - queueStatistics.widget.Refresh() - }) - - return queueStatistics -} - -func (s queueStatistics) add() { - *s.count += 1 - s.formatText(true) -} - -func (s queueStatistics) remove() { - if *s.count == 0 { - return - } - *s.count -= 1 - s.formatText(true) -} - -func (s queueStatistics) formatText(refresh bool) { - s.widget.Text = s.title + ": " + strconv.FormatInt(*s.count, 10) - if refresh == true { - fyne.Do(func() { - s.widget.Refresh() - }) - } -} diff --git a/kernel/localizer.go b/kernel/localizer.go deleted file mode 100644 index 085fb69..0000000 --- a/kernel/localizer.go +++ /dev/null @@ -1,194 +0,0 @@ -package kernel - -import ( - "embed" - "encoding/json" - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/lang" - "golang.org/x/text/language" -) - -//go:embed translations -var translations embed.FS - -var supportedLanguages = map[string]Lang{ - "ru": {Code: "ru", Title: "Русский"}, - "kk": {Code: "kk", Title: "Қазақ Тілі"}, - "en": {Code: "en", Title: "English"}, -} - -type LocalizerContract interface { - IsStartWithLanguageSelection() bool - GetMessage(key string, data ...any) string - GetLanguages() []Lang - GetCurrentLanguage() Lang - SetCurrentLanguage(selectLang Lang, isSaveSetting bool) error - AddChangeCallback(messageID string, callback func(text string)) -} - -type Lang struct { - Code string - Title string -} - -type changeCallback struct { - messageID string - callback func(text string) -} - -type Localizer struct { - setting SettingLanguageContract - currentLang Lang - changeCallbacks map[int]*changeCallback - isStartWithLanguageSelection bool -} - -func newLocalizer(app fyne.App) (*Localizer, error) { - setting := newSettingLanguage(app) - currentLanguage, isLanguageNotSupported := setting.GetLang() - - localizer := &Localizer{ - setting: setting, - changeCallbacks: map[int]*changeCallback{}, - isStartWithLanguageSelection: isLanguageNotSupported, - } - - err := localizer.SetCurrentLanguage(currentLanguage, false) - if err != nil { - return nil, err - } - - return localizer, nil -} - -func (l *Localizer) IsStartWithLanguageSelection() bool { - return l.isStartWithLanguageSelection -} - -func (l *Localizer) GetMessage(key string, data ...any) string { - return lang.L(key, data...) -} - -func (l *Localizer) GetLanguages() []Lang { - return getLanguages() -} - -func (l *Localizer) GetCurrentLanguage() Lang { - return l.currentLang -} - -func (l *Localizer) SetCurrentLanguage(selectLang Lang, isSaveSetting bool) error { - l.currentLang = selectLang - - translationsData, err := l.getTranslations(selectLang) - if err != nil { - return err - } - - name := lang.SystemLocale().LanguageString() - err = lang.AddTranslations(fyne.NewStaticResource(name+".json", translationsData)) - if err != nil { - return err - } - - if isSaveSetting { - l.setting.SetLang(selectLang) - } - - l.eventSetCurrentLanguage() - return nil -} - -func (l *Localizer) AddChangeCallback(messageID string, callback func(text string)) { - l.changeCallbacks[len(l.changeCallbacks)] = &changeCallback{messageID: messageID, callback: callback} -} - -func (l *Localizer) eventSetCurrentLanguage() { - for _, changeCallback := range l.changeCallbacks { - text := l.GetMessage(changeCallback.messageID) - changeCallback.callback(text) - } -} - -func (l *Localizer) getTranslations(language Lang) ([]byte, error) { - baseJson, err := translations.ReadFile("translations/base." + language.Code + ".json") - if err != nil { - return nil, err - } - appJson, err := translations.ReadFile("translations/app." + language.Code + ".json") - if err != nil { - return nil, err - } - - return l.mergeTranslations(baseJson, appJson) -} - -func (l *Localizer) mergeTranslations(baseJson []byte, appJson []byte) ([]byte, error) { - base := map[string]interface{}{} - custom := map[string]interface{}{} - err := json.Unmarshal(baseJson, &base) - if err != nil { - return nil, err - } - err = json.Unmarshal(appJson, &custom) - if err != nil { - return nil, err - } - - for k, v := range custom { - base[k] = v - } - return json.Marshal(base) -} - -func getLanguages() []Lang { - items := []Lang{} - for _, item := range supportedLanguages { - items = append(items, item) - } - return items -} - -type SettingLanguageContract interface { - GetLang() (currentLang Lang, isLanguageNotSupported bool) - SetLang(language Lang) -} - -type SettingLanguage struct { - app fyne.App -} - -func newSettingLanguage(app fyne.App) *SettingLanguage { - return &SettingLanguage{ - app: app, - } -} - -func (s *SettingLanguage) GetLang() (currentLang Lang, isLanguageNotSupported bool) { - languageCode := s.app.Preferences().String("language") - currentLang = supportedLanguages["ru"] - - if languageCode == "" { - languageTag, err := language.Parse(lang.SystemLocale().LanguageString()) - if err != nil { - return currentLang, true - } - base, _ := languageTag.Base() - languageCode = base.String() - } - - if findLang, ok := findSupportedLanguage(languageCode); ok { - return findLang, false - } - - return currentLang, true -} - -func (s *SettingLanguage) SetLang(language Lang) { - s.app.Preferences().SetString("language", language.Code) -} - -func findSupportedLanguage(code string) (Lang, bool) { - lang, ok := supportedLanguages[code] - return lang, ok -} diff --git a/kernel/progressbar.go b/kernel/progressbar.go deleted file mode 100644 index 95d4ce5..0000000 --- a/kernel/progressbar.go +++ /dev/null @@ -1,245 +0,0 @@ -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" - "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("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("inProgressQueue"), - completed: localizerService.GetMessage("completedQueue"), - error: localizerService.GetMessage("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/queue.go b/kernel/queue.go deleted file mode 100644 index 6d2f6c4..0000000 --- a/kernel/queue.go +++ /dev/null @@ -1,125 +0,0 @@ -package kernel - -import ( - "errors" -) - -type Queue struct { - Setting *ConvertSetting - Status StatusContract - Error error -} - -type StatusContract interface { - Name() string - Ordinal() int -} - -const ( - Waiting = iota - InProgress - Completed - Error -) - -type StatusType uint - -var statusTypeStrings = []string{ - "waiting", - "inProgress", - "completed", - "error", -} - -func (status StatusType) Name() string { - return statusTypeStrings[status] -} - -func (status StatusType) Ordinal() int { - return int(status) -} - -type QueueListenerContract interface { - Add(key int, queue *Queue) - Remove(key int) -} - -type QueueListContract interface { - AddListener(queueListener QueueListenerContract) - GetItems() map[int]*Queue - Add(setting *ConvertSetting) - Remove(key int) - GetItem(key int) (*Queue, error) - Next() (key int, queue *Queue) -} - -type QueueList struct { - currentKey *int - items map[int]*Queue - queueListener map[int]QueueListenerContract -} - -func NewQueueList() *QueueList { - currentKey := 0 - return &QueueList{ - currentKey: ¤tKey, - items: map[int]*Queue{}, - queueListener: map[int]QueueListenerContract{}, - } -} - -func (l QueueList) GetItems() map[int]*Queue { - return l.items -} - -func (l QueueList) Add(setting *ConvertSetting) { - queue := Queue{ - Setting: setting, - Status: StatusType(Waiting), - } - - *l.currentKey += 1 - l.items[*l.currentKey] = &queue - l.eventAdd(*l.currentKey, &queue) -} - -func (l QueueList) Remove(key int) { - if _, ok := l.items[key]; ok { - delete(l.items, key) - l.eventRemove(key) - } -} - -func (l QueueList) GetItem(key int) (*Queue, error) { - if item, ok := l.items[key]; ok { - return item, nil - } - - return nil, errors.New("key not found") -} - -func (l QueueList) AddListener(queueListener QueueListenerContract) { - l.queueListener[len(l.queueListener)] = queueListener -} - -func (l QueueList) eventAdd(key int, queue *Queue) { - for _, listener := range l.queueListener { - listener.Add(key, queue) - } -} - -func (l QueueList) eventRemove(key int) { - for _, listener := range l.queueListener { - listener.Remove(key) - } -} - -func (l QueueList) Next() (key int, queue *Queue) { - statusWaiting := StatusType(Waiting) - for key, item := range l.items { - if item.Status == statusWaiting { - return key, item - } - } - return -1, nil -} diff --git a/kernel/right_tabs.go b/kernel/right_tabs.go deleted file mode 100644 index 9b41880..0000000 --- a/kernel/right_tabs.go +++ /dev/null @@ -1,75 +0,0 @@ -package kernel - -import ( - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/container" -) - -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("addedFilesTitle"), addedFilesContainer) - localizerService.AddChangeCallback("addedFilesTitle", func(text string) { - addedFilesTab.Text = text - }) - - fileQueueContainer := container.NewVBox() - fileQueueTab := container.NewTabItem(localizerService.GetMessage("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/translations/app.en.json b/kernel/translations/app.en.json deleted file mode 100644 index 9919f8d..0000000 --- a/kernel/translations/app.en.json +++ /dev/null @@ -1,143 +0,0 @@ -{ - "AlsoUsedProgram": "The program also uses:", - "about": "About", - "aboutText": "A simple interface for the FFmpeg console utility. \nBut I am not the author of the FFmpeg utility itself.", - "addedFilesTitle": "Added files", - "autoClearAfterAddingToQueue": "Auto-clear after adding to queue", - "buttonDownloadFFmpeg": "Download FFmpeg automatically", - "buttonForSelectedDirTitle": "Save to folder:", - "cancel": "Cancel", - "changeFFPath": "FFmpeg, FFprobe and FFplay", - "changeLanguage": "Change language", - "checkboxOverwriteOutputFilesTitle": "Allow file to be overwritten", - "choose": "choose", - "clearAll": "Clear List", - "completedQueue": "Completed", - "converterVideoFilesSubmitTitle": "Convert", - "converterVideoFilesTitle": "Video, audio and picture converter", - "download": "Download", - "downloadFFmpegFromSite": "Will be downloaded from the site:", - "downloadRun": "Downloading...", - "dragAndDropFiles": "drag and drop files", - "encoderGroupAudio": "Audio", - "encoderGroupImage": "Images", - "encoderGroupVideo": "Video", - "encoder_apng": "APNG image", - "encoder_bmp": "BMP image", - "encoder_flv": "FLV", - "encoder_gif": "GIF image", - "encoder_h264_nvenc": "H.264 with NVIDIA support", - "encoder_libmp3lame": "libmp3lame MP3 (MPEG audio layer 3)", - "encoder_libshine": "libshine MP3 (MPEG audio layer 3)", - "encoder_libtwolame": "libtwolame MP2 (MPEG audio layer 2)", - "encoder_libvpx": "libvpx VP8 (codec vp8)", - "encoder_libvpx-vp9": "libvpx VP9 (codec vp9)", - "encoder_libwebp": "libwebp WebP image", - "encoder_libwebp_anim": "libwebp_anim WebP image", - "encoder_libx264": "H.264 libx264", - "encoder_libx265": "H.265 libx265", - "encoder_libxvid": "libxvidcore MPEG-4 part 2", - "encoder_mjpeg": "MJPEG (Motion JPEG)", - "encoder_mp2": "MP2 (MPEG audio layer 2)", - "encoder_mp2fixed": "MP2 fixed point (MPEG audio layer 2)", - "encoder_mpeg1video": "MPEG-1", - "encoder_mpeg2video": "MPEG-2", - "encoder_mpeg4": "MPEG-4 part 2", - "encoder_msmpeg4": "MPEG-4 part 2 Microsoft variant version 3", - "encoder_msmpeg4v2": "MPEG-4 part 2 Microsoft variant version 2", - "encoder_msvideo1": "Microsoft Video-1", - "encoder_png": "PNG image", - "encoder_qtrle": "QuickTime Animation (RLE) video", - "encoder_sgi": "SGI image", - "encoder_tiff": "TIFF image", - "encoder_wmav1": "Windows Media Audio 1", - "encoder_wmav2": "Windows Media Audio 2", - "encoder_wmv1": "Windows Media Video 7", - "encoder_wmv2": "Windows Media Video 8", - "encoder_xbm": "XBM (X BitMap) image", - "error": "An error has occurred!", - "errorConverter": "Couldn't convert video", - "errorDragAndDropFile": "Not all files were added", - "errorFFmpeg": "this is not FFmpeg", - "errorFFmpegVersion": "Could not determine FFmpeg version", - "errorFFplay": "this is not FFplay", - "errorFFplayVersion": "Could not determine FFplay version", - "errorFFprobe": "this is not FFprobe", - "errorFFprobeVersion": "Failed to determine FFprobe version", - "errorNoFilesAddedForConversion": "There are no files to convert", - "errorQueue": "Error", - "errorSelectedEncoder": "Converter not selected", - "errorSelectedFolderSave": "No save folder selected!", - "errorSelectedFormat": "File extension not selected", - "exit": "Exit", - "ffmpegLGPL": "This software uses libraries from the **FFmpeg** project under the **[LGPLv2.1](http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html)**.", - "ffmpegTrademark": "**FFmpeg** is a trademark of **[Fabrice Bellard](http://bellard.org/)**, originator of the **[FFmpeg](https://ffmpeg.org/about.html)** project.", - "fileForConversionTitle": "File:", - "fileQueueTitle": "Queue", - "formPreset": "Preset", - "gratitude": "Gratitude", - "gratitudeText": "I sincerely thank you for your invaluable\n\r and timely assistance:", - "help": "Help", - "helpFFplay": "FFplay Player Keys", - "helpFFplayActivateFrameStepMode": "Activate frame-by-frame mode.", - "helpFFplayCycleVideoFiltersOrShowModes": "A cycle of video filters or display modes.", - "helpFFplayDecreaseVolume": "Decrease the volume.", - "helpFFplayDescription": "Description", - "helpFFplayDoubleClickLeftMouseButton": "double click\nleft mouse button", - "helpFFplayIncreaseVolume": "Increase the volume.", - "helpFFplayKeyDown": "down", - "helpFFplayKeyHoldS": "hold S", - "helpFFplayKeyLeft": "left", - "helpFFplayKeyRight": "right", - "helpFFplayKeySpace": "SPACE", - "helpFFplayKeyUp": "up", - "helpFFplayKeys": "Keys", - "helpFFplayPause": "Pause or continue playing.", - "helpFFplayQuit": "Close the player.", - "helpFFplaySeekBForward10Minutes": "Fast forward 10 minutes.", - "helpFFplaySeekBForward1Minute": "Fast forward 1 minute.", - "helpFFplaySeekBackward10Minutes": "Rewind 10 minutes.", - "helpFFplaySeekBackward10Seconds": "Rewind 10 seconds.", - "helpFFplaySeekBackward1Minute": "Rewind 1 minute.", - "helpFFplaySeekForward10Seconds": "Fast forward 10 seconds.", - "helpFFplayToggleFullScreen": "Switch to full screen or exit full screen.", - "helpFFplayToggleMute": "Mute or unmute.", - "inProgressQueue": "In Progress", - "languageSelectionFormHead": "Switch language", - "languageSelectionHead": "Choose language", - "licenseLink": "License information", - "licenseLinkOther": "Licenses from other products used in the program", - "menuSettingsLanguage": "Language", - "menuSettingsTheme": "Theme", - "or": "or", - "parameterCheckbox": "Enable option", - "pathToFfmpeg": "Path to FFmpeg:", - "pathToFfplay": "Path to FFplay:", - "pathToFfprobe": "Path to FFprobe:", - "preset_fast": "fast (slower than \"faster\", but the file will weigh less)", - "preset_faster": "faster (slower than \"veryfast\", but the file will weigh less)", - "preset_medium": "medium (slower than \"fast\", but the file will weigh less)", - "preset_placebo": "placebo (not recommended)", - "preset_slow": "slow (slower than \"medium\", but the file will weigh less)", - "preset_slower": "slower (slower than \"slow\", but the file will weigh less)", - "preset_superfast": "superfast (slower than \"ultrafast\", but the file will weigh less)", - "preset_ultrafast": "ultrafast (fast, but the file will weigh a lot)", - "preset_veryfast": "veryfast (slower than \"superfast\", but the file will weigh less)", - "preset_veryslow": "veryslow (slower than \"slower\", but the file will weigh less)", - "programmLink": "Project website", - "programmVersion": "**Program version:** {{.Version}}", - "queue": "Queue", - "save": "Save", - "selectEncoder": "Encoder:", - "selectFFPathTitle": "Specify the path to FFmpeg and FFprobe", - "selectFormat": "File extension:", - "settings": "Settings", - "testFF": "Checking FFmpeg for serviceability...", - "themesNameDark": "Dark", - "themesNameDefault": "Default", - "themesNameLight": "Light", - "titleDownloadLink": "You can download it from here", - "total": "Total", - "unzipRun": "Unpacked...", - "waitingQueue": "Waiting" -} \ No newline at end of file diff --git a/kernel/translations/app.kk.json b/kernel/translations/app.kk.json deleted file mode 100644 index 6fb1b81..0000000 --- a/kernel/translations/app.kk.json +++ /dev/null @@ -1,143 +0,0 @@ -{ - "AlsoUsedProgram": "Бағдарлама сонымен қатар пайдаланады:", - "about": "Бағдарлама туралы", - "aboutText": "FFmpeg консоль утилитасы үшін қарапайым интерфейс. \nБірақ мен FFmpeg утилитасының авторы емеспін.", - "addedFilesTitle": "Қосылған файлдар", - "autoClearAfterAddingToQueue": "Кезекке қосқаннан кейін тазалаңыз", - "buttonDownloadFFmpeg": "FFmpeg автоматты түрде жүктеп алыңыз", - "buttonForSelectedDirTitle": "Қалтаға сақтаңыз:", - "cancel": "Болдырмау", - "changeFFPath": "FFmpeg, FFprobe және FFplay", - "changeLanguage": "Тілді өзгерту", - "checkboxOverwriteOutputFilesTitle": "Файлды қайта жазуға рұқсат беріңіз", - "choose": "таңдау", - "clearAll": "Тізімді өшіру", - "completedQueue": "Дайын", - "converterVideoFilesSubmitTitle": "Файлды түрлендіру", - "converterVideoFilesTitle": "Бейне, аудио және суретті түрлендіргіш", - "download": "Жүктеп алу", - "downloadFFmpegFromSite": "Сайттан жүктеледі:", - "downloadRun": "Жүктеп алынуда...", - "dragAndDropFiles": "файлдарды сүйреп апарыңыз", - "encoderGroupAudio": "Аудио", - "encoderGroupImage": "Суреттер", - "encoderGroupVideo": "Бейне", - "encoder_apng": "APNG image", - "encoder_bmp": "BMP image", - "encoder_flv": "FLV", - "encoder_gif": "GIF image", - "encoder_h264_nvenc": "NVIDIA қолдауымен H.264", - "encoder_libmp3lame": "libmp3lame MP3 (MPEG audio layer 3)", - "encoder_libshine": "libshine MP3 (MPEG audio layer 3)", - "encoder_libtwolame": "libtwolame MP2 (MPEG audio layer 2)", - "encoder_libvpx": "libvpx VP8 (codec vp8)", - "encoder_libvpx-vp9": "libvpx VP9 (codec vp9)", - "encoder_libwebp": "libwebp WebP image", - "encoder_libwebp_anim": "libwebp_anim WebP image", - "encoder_libx264": "H.264 libx264", - "encoder_libx265": "H.265 libx265", - "encoder_libxvid": "libxvidcore MPEG-4 part 2", - "encoder_mjpeg": "MJPEG (Motion JPEG)", - "encoder_mp2": "MP2 (MPEG audio layer 2)", - "encoder_mp2fixed": "MP2 fixed point (MPEG audio layer 2)", - "encoder_mpeg1video": "MPEG-1", - "encoder_mpeg2video": "MPEG-2", - "encoder_mpeg4": "MPEG-4 part 2", - "encoder_msmpeg4": "MPEG-4 part 2 Microsoft variant version 3", - "encoder_msmpeg4v2": "MPEG-4 part 2 Microsoft variant version 2", - "encoder_msvideo1": "Microsoft Video-1", - "encoder_png": "PNG image", - "encoder_qtrle": "QuickTime Animation (RLE) video", - "encoder_sgi": "SGI image", - "encoder_tiff": "TIFF image", - "encoder_wmav1": "Windows Media Audio 1", - "encoder_wmav2": "Windows Media Audio 2", - "encoder_wmv1": "Windows Media Video 7", - "encoder_wmv2": "Windows Media Video 8", - "encoder_xbm": "XBM (X BitMap) image", - "error": "Қате орын алды!", - "errorConverter": "Бейнені түрлендіру мүмкін болмады", - "errorDragAndDropFile": "Барлық файлдар қосылмаған", - "errorFFmpeg": "бұл FFmpeg емес", - "errorFFmpegVersion": "FFmpeg нұсқасын анықтау мүмкін болмады", - "errorFFplay": "бұл FFplay емес", - "errorFFplayVersion": "FFplay нұсқасын анықтау мүмкін болмады", - "errorFFprobe": "бұл FFprobe емес", - "errorFFprobeVersion": "FFprobe нұсқасын анықтау мүмкін болмады", - "errorNoFilesAddedForConversion": "Түрлендіруге арналған файлдар жоқ", - "errorQueue": "Қате", - "errorSelectedEncoder": "Түрлендіргіш таңдалмаған", - "errorSelectedFolderSave": "Сақтау қалтасы таңдалмаған!", - "errorSelectedFormat": "Файл кеңейтімі таңдалмаған", - "exit": "Шығу", - "ffmpegLGPL": "Бұл бағдарламалық құрал **[LGPLv2.1](http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html)** астында **FFmpeg** жобасының кітапханаларын пайдаланады.", - "ffmpegTrademark": "FFmpeg — **[FFmpeg](https://ffmpeg.org/about.html)** жобасын жасаушы **[Fabrice Bellard](http://bellard.org/)** сауда белгісі.", - "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» қарағанда баяуырақ, бірақ файлдың салмағы аз болады)", - "preset_medium": "medium («fast» қарағанда баяуырақ, бірақ файлдың салмағы аз болады)", - "preset_placebo": "placebo (ұсынылмайды)", - "preset_slow": "slow («medium» қарағанда баяуырақ, бірақ файлдың салмағы аз болады)", - "preset_slower": "slower («slow» қарағанда баяуырақ, бірақ файлдың салмағы аз болады)", - "preset_superfast": "superfast («ultrafast» қарағанда баяуырақ, бірақ файлдың салмағы аз болады)", - "preset_ultrafast": "ultrafast (жылдам, бірақ файлдың салмағы көп болады)", - "preset_veryfast": "veryfast («superfast» қарағанда баяуырақ, бірақ файлдың салмағы аз болады)", - "preset_veryslow": "veryslow («slower» қарағанда баяуырақ, бірақ файлдың салмағы аз болады)", - "programmLink": "Жобаның веб-сайты", - "programmVersion": "**Бағдарлама нұсқасы:** {{.Version}}", - "queue": "Кезек", - "save": "Сақтау", - "selectEncoder": "Кодировщик:", - "selectFFPathTitle": "FFmpeg және FFprobe жолын көрсетіңіз", - "selectFormat": "Файл кеңейтімі:", - "settings": "Параметрлер", - "testFF": "FFmpeg функционалдығы тексерілуде...", - "themesNameDark": "Қараңғы тақырып", - "themesNameDefault": "Әдепкі бойынша", - "themesNameLight": "Жеңіл тақырып", - "titleDownloadLink": "Сіз оны осы жерден жүктей аласыз", - "total": "Барлығы", - "unzipRun": "Орамнан шығарылуда...", - "waitingQueue": "Күту" -} \ No newline at end of file diff --git a/kernel/translations/app.ru.json b/kernel/translations/app.ru.json deleted file mode 100644 index 20a5da8..0000000 --- a/kernel/translations/app.ru.json +++ /dev/null @@ -1,143 +0,0 @@ -{ - "AlsoUsedProgram": "Также в программе используется:", - "about": "О программе", - "aboutText": "Простенький интерфейс для консольной утилиты FFmpeg. \nНо я не являюсь автором самой утилиты FFmpeg.", - "addedFilesTitle": "Добавленные файлы", - "autoClearAfterAddingToQueue": "Очищать после добавления в очередь", - "buttonDownloadFFmpeg": "Скачать автоматически FFmpeg", - "buttonForSelectedDirTitle": "Сохранить в папку:", - "cancel": "Отмена", - "changeFFPath": "FFmpeg, FFprobe и FFplay", - "changeLanguage": "Поменять язык", - "checkboxOverwriteOutputFilesTitle": "Разрешить перезаписать файл", - "choose": "выбрать", - "clearAll": "Очистить список", - "completedQueue": "Готово", - "converterVideoFilesSubmitTitle": "Конвертировать", - "converterVideoFilesTitle": "Конвертер видео, аудио и картинок", - "download": "Скачать", - "downloadFFmpegFromSite": "Будет скачано с сайта:", - "downloadRun": "Скачивается...", - "dragAndDropFiles": "перетащить файлы", - "encoderGroupAudio": "Аудио", - "encoderGroupImage": "Картинки", - "encoderGroupVideo": "Видео", - "encoder_apng": "APNG image", - "encoder_bmp": "BMP image", - "encoder_flv": "FLV", - "encoder_gif": "GIF image", - "encoder_h264_nvenc": "H.264 с поддержкой NVIDIA", - "encoder_libmp3lame": "libmp3lame MP3 (MPEG audio layer 3)", - "encoder_libshine": "libshine MP3 (MPEG audio layer 3)", - "encoder_libtwolame": "libtwolame MP2 (MPEG audio layer 2)", - "encoder_libvpx": "libvpx VP8 (codec vp8)", - "encoder_libvpx-vp9": "libvpx VP9 (codec vp9)", - "encoder_libwebp": "libwebp WebP image", - "encoder_libwebp_anim": "libwebp_anim WebP image", - "encoder_libx264": "H.264 libx264", - "encoder_libx265": "H.265 libx265", - "encoder_libxvid": "libxvidcore MPEG-4 part 2", - "encoder_mjpeg": "MJPEG (Motion JPEG)", - "encoder_mp2": "MP2 (MPEG audio layer 2)", - "encoder_mp2fixed": "MP2 fixed point (MPEG audio layer 2)", - "encoder_mpeg1video": "MPEG-1", - "encoder_mpeg2video": "MPEG-2", - "encoder_mpeg4": "MPEG-4 part 2", - "encoder_msmpeg4": "MPEG-4 part 2 Microsoft variant version 3", - "encoder_msmpeg4v2": "MPEG-4 part 2 Microsoft variant version 2", - "encoder_msvideo1": "Microsoft Video-1", - "encoder_png": "PNG image", - "encoder_qtrle": "QuickTime Animation (RLE) video", - "encoder_sgi": "SGI image", - "encoder_tiff": "TIFF image", - "encoder_wmav1": "Windows Media Audio 1", - "encoder_wmav2": "Windows Media Audio 2", - "encoder_wmv1": "Windows Media Video 7", - "encoder_wmv2": "Windows Media Video 8", - "encoder_xbm": "XBM (X BitMap) image", - "error": "Произошла ошибка!", - "errorConverter": "не смогли отконвертировать видео", - "errorDragAndDropFile": "Не все файлы добавились", - "errorFFmpeg": "это не FFmpeg", - "errorFFmpegVersion": "Не смогли определить версию FFmpeg", - "errorFFplay": "это не FFplay", - "errorFFplayVersion": "Не смогли определить версию FFplay", - "errorFFprobe": "это не FFprobe", - "errorFFprobeVersion": "Не смогли определить версию FFprobe", - "errorNoFilesAddedForConversion": "Нет файлов для конвертации", - "errorQueue": "Ошибка", - "errorSelectedEncoder": "Конвертер не выбран", - "errorSelectedFolderSave": "Папка для сохранения не выбрана!", - "errorSelectedFormat": "Расширение файла не выбрана", - "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, но будет файл и меньше весить)", - "preset_medium": "medium (медленней чем fast, но будет файл и меньше весить)", - "preset_placebo": "placebo (не рекомендуется)", - "preset_slow": "slow (медленней чем medium, но будет файл и меньше весить)", - "preset_slower": "slower (медленней чем slow, но будет файл и меньше весить)", - "preset_superfast": "superfast (медленней чем ultrafast, но будет файл и меньше весить)", - "preset_ultrafast": "ultrafast (быстро, но файл будет много весить)", - "preset_veryfast": "veryfast (медленней чем superfast, но будет файл и меньше весить)", - "preset_veryslow": "veryslow (медленней чем slower, но будет файл и меньше весить)", - "programmLink": "Сайт проекта", - "programmVersion": "**Версия программы:** {{.Version}}", - "queue": "Очередь", - "save": "Сохранить", - "selectEncoder": "Кодировщик:", - "selectFFPathTitle": "Укажите путь к FFmpeg и к FFprobe", - "selectFormat": "Расширение файла:", - "settings": "Настройки", - "testFF": "Проверка FFmpeg на работоспособность...", - "themesNameDark": "Тёмная", - "themesNameDefault": "По умолчанию", - "themesNameLight": "Светлая", - "titleDownloadLink": "Скачать можно от сюда", - "total": "Всего", - "unzipRun": "Распаковывается...", - "waitingQueue": "В очереди" -} \ No newline at end of file diff --git a/kernel/translations/base.en.json b/kernel/translations/base.en.json deleted file mode 100644 index 3f5dd3d..0000000 --- a/kernel/translations/base.en.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "Advanced": "Advanced", - "Cancel": "Cancel", - "Confirm": "Confirm", - "Copy": "Copy", - "Create Folder": "Create Folder", - "Cut": "Cut", - "Enter filename": "Enter filename", - "Error": "Error", - "Favourites": "Favourites", - "File": "File", - "Folder": "Folder", - "New Folder": "New Folder", - "No": "No", - "OK": "OK", - "Open": "Open", - "Paste": "Paste", - "Quit": "Quit", - "Redo": "Redo", - "Save": "Save", - "Select all": "Select all", - "Show Hidden Files": "Show Hidden Files", - "Undo": "Undo", - "Yes": "Yes", - "file.name": { - "other": "Name" - }, - "file.parent": { - "other": "Parent" - }, - "friday": "Friday", - "friday.short": "Fri", - "monday": "Monday", - "monday.short": "Mon", - "saturday": "Saturday", - "saturday.short": "Sat", - "sunday": "Sunday", - "sunday.short": "Sun", - "thursday": "Thursday", - "thursday.short": "Thu", - "tuesday": "Tuesday", - "tuesday.short": "Tue", - "wednesday": "Wednesday", - "wednesday.short": "Wed" -} \ No newline at end of file diff --git a/kernel/translations/base.kk.json b/kernel/translations/base.kk.json deleted file mode 100644 index 469566b..0000000 --- a/kernel/translations/base.kk.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "Advanced": "Кеңейтілген", - "Cancel": "Бас тарту", - "Confirm": "Растау", - "Copy": "Көшіру", - "Create Folder": "Қалта жасау", - "Cut": "Кесу", - "Enter filename": "Файл атауын енгізіңіз", - "Error": "Қате", - "Favourites": "Таңдаулылар", - "File": "Файл", - "Folder": "Қалта", - "New Folder": "Жаңа қалта", - "No": "Жоқ", - "OK": "ОК", - "Open": "Ашу", - "Paste": "Кірістіру", - "Quit": "Шығу", - "Redo": "Қайталау", - "Save": "Сақтау", - "Select all": "Барлығын таңдаңыз", - "Show Hidden Files": "Жасырын файлдарды көрсету", - "Undo": "Бас тарту", - "Yes": "Иә", - "file.name": { - "other": "Аты" - }, - "file.parent": { - "other": "Жоғары" - }, - "friday": "Жұма", - "friday.short": "Жұ", - "monday": "Дүйсенбі", - "monday.short": "Дү", - "saturday": "Сенбі", - "saturday.short": "Сен", - "sunday": "Жексенбі", - "sunday.short": "Же", - "thursday": "Сейсенбі", - "thursday.short": "Се", - "tuesday": "Бейсенбі", - "tuesday.short": "Бе", - "wednesday": "Сәрсенбі", - "wednesday.short": "Сә" -} \ No newline at end of file diff --git a/kernel/translations/base.ru.json b/kernel/translations/base.ru.json deleted file mode 100644 index ac22058..0000000 --- a/kernel/translations/base.ru.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "Advanced": "Расширенные", - "Cancel": "Отмена", - "Confirm": "Подтвердить", - "Copy": "Копировать", - "Create Folder": "Создать папку", - "Cut": "Вырезать", - "Enter filename": "Введите имя файла", - "Error": "Ошибка", - "Favourites": "Избранное", - "File": "Файл", - "Folder": "Папка", - "New Folder": "Новая папка", - "No": "Нет", - "OK": "ОК", - "Open": "Открыть", - "Paste": "Вставить", - "Quit": "Выйти", - "Redo": "Повторить", - "Save": "Сохранить", - "Select all": "Выбрать всё", - "Show Hidden Files": "Показать скрытые файлы", - "Undo": "Отменить", - "Yes": "Да", - "file.name": { - "other": "Имя" - }, - "file.parent": { - "other": "Вверх" - }, - "friday": "Пятница", - "friday.short": "Пт", - "monday": "Понедельник", - "monday.short": "Пн", - "saturday": "Суббота", - "saturday.short": "Сб", - "sunday": "Воскресенье", - "sunday.short": "Вс", - "thursday": "Вторник", - "thursday.short": "Вт", - "tuesday": "Четверг", - "tuesday.short": "Чт", - "wednesday": "Среда", - "wednesday.short": "Ср" -} \ No newline at end of file diff --git a/kernel/window.go b/kernel/window.go deleted file mode 100644 index cec7b5b..0000000 --- a/kernel/window.go +++ /dev/null @@ -1,89 +0,0 @@ -package kernel - -import ( - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/dialog" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/helper" - "time" -) - -type WindowContract interface { - SetContent(content fyne.CanvasObject) - SetMainMenu(menu *fyne.MainMenu) - NewFileOpen(callback func(fyne.URIReadCloser, error), location fyne.ListableURI) *dialog.FileDialog - NewFolderOpen(callback func(fyne.ListableURI, error), location fyne.ListableURI) *dialog.FileDialog - SetOnDropped(callback func(position fyne.Position, uris []fyne.URI)) - ShowAndRun() - GetLayout() LayoutContract -} - -type Window struct { - windowFyne fyne.Window - layout LayoutContract -} - -func newWindow(w fyne.Window, layout LayoutContract) Window { - windowSize := fyne.Size{Width: 1039, Height: 599} - w.Resize(windowSize) - w.CenterOnScreen() - - fyne.Do(func() { - /** - * Bug fixed. - * When starting the program, sometimes the window was displayed incorrectly. - */ - windowSize.Width += 1 - windowSize.Height += 1 - time.Sleep(time.Millisecond * 500) - w.Resize(windowSize) - }) - - return Window{ - windowFyne: w, - layout: layout, - } -} - -func (w Window) SetContent(content fyne.CanvasObject) { - fyne.Do(func() { - w.windowFyne.SetContent(w.layout.SetContent(content)) - }) -} - -func (w Window) NewFileOpen(callback func(fyne.URIReadCloser, error), location fyne.ListableURI) *dialog.FileDialog { - fileDialog := dialog.NewFileOpen(callback, w.windowFyne) - helper.FileDialogResize(fileDialog, w.windowFyne) - fileDialog.Show() - if location != nil { - fileDialog.SetLocation(location) - } - return fileDialog -} - -func (w Window) NewFolderOpen(callback func(fyne.ListableURI, error), location fyne.ListableURI) *dialog.FileDialog { - fileDialog := dialog.NewFolderOpen(callback, w.windowFyne) - helper.FileDialogResize(fileDialog, w.windowFyne) - fileDialog.Show() - if location != nil { - fileDialog.SetLocation(location) - } - return fileDialog -} - -func (w Window) SetMainMenu(menu *fyne.MainMenu) { - w.windowFyne.SetMainMenu(menu) -} - -func (w Window) ShowAndRun() { - w.windowFyne.ShowAndRun() -} - -func (w Window) GetLayout() LayoutContract { - return w.layout -} - -func (w Window) SetOnDropped(callback func(position fyne.Position, uris []fyne.URI)) { - fyne.Do(func() { - w.windowFyne.SetOnDropped(callback) - }) -} diff --git a/localizer/view.go b/localizer/view.go deleted file mode 100644 index 9233b6d..0000000 --- a/localizer/view.go +++ /dev/null @@ -1,69 +0,0 @@ -package localizer - -import ( - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/widget" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" -) - -type ViewContract interface { - LanguageSelection(funcSelected func(lang kernel.Lang)) -} - -type View struct { - app kernel.AppContract -} - -func NewView(app kernel.AppContract) *View { - return &View{ - app: app, - } -} - -func (v View) LanguageSelection(funcSelected func(lang kernel.Lang)) { - languages := v.app.GetLocalizerService().GetLanguages() - listView := widget.NewList( - func() int { - return len(languages) - }, - func() fyne.CanvasObject { - return widget.NewLabel("template") - }, - func(i widget.ListItemID, o fyne.CanvasObject) { - block := o.(*widget.Label) - block.SetText(languages[i].Title) - }) - listView.OnSelected = func(id widget.ListItemID) { - _ = v.app.GetLocalizerService().SetCurrentLanguage(languages[id], true) - funcSelected(languages[id]) - } - - messageHead := v.app.GetLocalizerService().GetMessage("languageSelectionHead") - v.app.GetWindow().SetContent(widget.NewCard(messageHead, "", listView)) -} - -func LanguageSelectionForm(localizerService kernel.LocalizerContract, funcSelected func(lang kernel.Lang)) fyne.CanvasObject { - languages := localizerService.GetLanguages() - currentLanguage := localizerService.GetCurrentLanguage() - listView := widget.NewList( - func() int { - return len(languages) - }, - func() fyne.CanvasObject { - return widget.NewLabel("template") - }, - func(i widget.ListItemID, o fyne.CanvasObject) { - block := o.(*widget.Label) - block.SetText(languages[i].Title) - if languages[i].Code == currentLanguage.Code { - block.TextStyle = fyne.TextStyle{Bold: true} - } - }) - listView.OnSelected = func(id widget.ListItemID) { - _ = localizerService.SetCurrentLanguage(languages[id], true) - funcSelected(languages[id]) - } - - messageHead := localizerService.GetMessage("languageSelectionFormHead") - return widget.NewCard(messageHead, "", listView) -} diff --git a/menu/view.go b/menu/view.go deleted file mode 100644 index be4b4f4..0000000 --- a/menu/view.go +++ /dev/null @@ -1,774 +0,0 @@ -package menu - -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/internal/resources" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" - "golang.org/x/image/colornames" - "net/url" -) - -type ViewContract interface { - About(ffmpegVersion string, ffprobeVersion string, ffplayVersion string) - Gratitude() - HelpFFplay() -} - -type View struct { - app kernel.AppContract -} - -func NewView(app kernel.AppContract) *View { - return &View{ - app: app, - } -} - -func (v View) Gratitude() { - view := v.app.GetAppFyne().NewWindow(v.app.GetLocalizerService().GetMessage("gratitude")) - view.Resize(fyne.Size{Width: 500, Height: 400}) - view.SetFixedSize(true) - - image := canvas.NewImageFromResource(resources.IconAppLogoResource()) - image.SetMinSize(fyne.Size{Width: 100, Height: 100}) - image.FillMode = canvas.ImageFillContain - - gratitude := canvas.NewText(v.app.GetLocalizerService().GetMessage("gratitude"), colornames.Darkgreen) - gratitude.TextStyle = fyne.TextStyle{Bold: true} - gratitude.TextSize = 20 - - view.SetContent( - container.NewScroll(container.NewVBox( - container.NewBorder(nil, nil, container.NewVBox(image), nil, container.NewVBox( - gratitude, - widget.NewLabel(v.app.GetLocalizerService().GetMessage("gratitudeText")), - widget.NewLabel("Екатерина"), - widget.NewLabel("Евгений"), - )), - )), - ) - - view.CenterOnScreen() - view.Show() -} - -func (v View) About(ffmpegVersion string, ffprobeVersion string, ffplayVersion string) { - view := v.app.GetAppFyne().NewWindow(v.app.GetLocalizerService().GetMessage("about")) - view.Resize(fyne.Size{Width: 793, Height: 550}) - view.SetFixedSize(true) - - programmName := canvas.NewText(" GUI for FFmpeg", colornames.Darkgreen) - programmName.TextStyle = fyne.TextStyle{Bold: true} - programmName.TextSize = 20 - - programmLink := widget.NewHyperlink( - v.app.GetLocalizerService().GetMessage("programmLink"), - &url.URL{ - Scheme: "https", - Host: "gui-for-ffmpeg.projects.kor-elf.net", - Path: "/", - }, - ) - - licenseLink := widget.NewHyperlink( - v.app.GetLocalizerService().GetMessage("licenseLink"), - &url.URL{ - Scheme: "https", - Host: "git.kor-elf.net", - Path: "kor-elf/gui-for-ffmpeg/src/branch/main/LICENSE", - }, - ) - - licenseLinkOther := widget.NewHyperlink(v.app.GetLocalizerService().GetMessage( - "licenseLinkOther"), - &url.URL{ - Scheme: "https", - Host: "git.kor-elf.net", - Path: "kor-elf/gui-for-ffmpeg/src/branch/main/LICENSE-3RD-PARTY.txt", - }, - ) - - programmVersion := widget.NewRichTextFromMarkdown( - v.app.GetLocalizerService().GetMessage( - "programmVersion", - map[string]any{"Version": v.app.GetAppFyne().Metadata().Version}, - ), - ) - - aboutText := widget.NewRichText( - &widget.TextSegment{ - Text: v.app.GetLocalizerService().GetMessage("aboutText"), - }, - ) - image := canvas.NewImageFromResource(resources.IconAppLogoResource()) - image.SetMinSize(fyne.Size{Width: 100, Height: 100}) - image.FillMode = canvas.ImageFillContain - - ffmpegTrademark := widget.NewRichTextFromMarkdown(v.app.GetLocalizerService().GetMessage("ffmpegTrademark")) - ffmpegLGPL := widget.NewRichTextFromMarkdown(v.app.GetLocalizerService().GetMessage("ffmpegLGPL")) - - view.SetContent( - container.NewScroll(container.NewVBox( - container.NewBorder(nil, nil, container.NewVBox(image), nil, container.NewVBox( - programmName, - programmVersion, - aboutText, - ffmpegTrademark, - ffmpegLGPL, - v.getCopyright(), - container.NewHBox(programmLink, licenseLink), - container.NewHBox(licenseLinkOther), - )), - v.getAboutFfmpeg(ffmpegVersion), - v.getAboutFfprobe(ffprobeVersion), - v.getAboutFfplay(ffplayVersion), - widget.NewCard(v.app.GetLocalizerService().GetMessage("AlsoUsedProgram"), "", v.getOther()), - )), - ) - view.CenterOnScreen() - view.Show() -} - -func (v View) HelpFFplay() { - view := v.app.GetAppFyne().NewWindow(v.app.GetLocalizerService().GetMessage("helpFFplay")) - view.Resize(fyne.Size{Width: 800, Height: 550}) - view.SetFixedSize(true) - - data := [][]string{ - []string{ - v.app.GetLocalizerService().GetMessage("helpFFplayKeys"), - v.app.GetLocalizerService().GetMessage("helpFFplayDescription"), - }, - []string{ - "Q, ESC", - v.app.GetLocalizerService().GetMessage("helpFFplayQuit"), - }, - []string{ - "F, " + v.app.GetLocalizerService().GetMessage("helpFFplayDoubleClickLeftMouseButton"), - v.app.GetLocalizerService().GetMessage("helpFFplayToggleFullScreen"), - }, - []string{ - "P, " + - v.app.GetLocalizerService().GetMessage("helpFFplayKeySpace"), - v.app.GetLocalizerService().GetMessage("helpFFplayPause"), - }, - []string{ - "M", - v.app.GetLocalizerService().GetMessage("helpFFplayToggleMute"), - }, - []string{ - "9, /", - v.app.GetLocalizerService().GetMessage("helpFFplayDecreaseVolume"), - }, - []string{ - "0, *", - v.app.GetLocalizerService().GetMessage("helpFFplayIncreaseVolume"), - }, - []string{ - v.app.GetLocalizerService().GetMessage("helpFFplayKeyLeft"), - v.app.GetLocalizerService().GetMessage("helpFFplaySeekBackward10Seconds"), - }, - []string{ - v.app.GetLocalizerService().GetMessage("helpFFplayKeyRight"), - v.app.GetLocalizerService().GetMessage("helpFFplaySeekForward10Seconds"), - }, - []string{ - v.app.GetLocalizerService().GetMessage("helpFFplayKeyDown"), - v.app.GetLocalizerService().GetMessage("helpFFplaySeekBackward1Minute"), - }, - []string{ - v.app.GetLocalizerService().GetMessage("helpFFplayKeyUp"), - v.app.GetLocalizerService().GetMessage("helpFFplaySeekBForward1Minute"), - }, - []string{ - "Page Down", - v.app.GetLocalizerService().GetMessage("helpFFplaySeekBackward10Minutes"), - }, - []string{ - "Page Up", - v.app.GetLocalizerService().GetMessage("helpFFplaySeekBForward10Minutes"), - }, - []string{ - "S, " + v.app.GetLocalizerService().GetMessage("helpFFplayKeyHoldS"), - v.app.GetLocalizerService().GetMessage("helpFFplayActivateFrameStepMode"), - }, - []string{ - "W", - v.app.GetLocalizerService().GetMessage("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/)**.") -} - -func (v View) getAboutFfmpeg(version string) *fyne.Container { - programmName := canvas.NewText(" FFmpeg", colornames.Darkgreen) - programmName.TextStyle = fyne.TextStyle{Bold: true} - programmName.TextSize = 20 - - programmLink := widget.NewHyperlink(v.app.GetLocalizerService().GetMessage("programmLink"), &url.URL{ - Scheme: "https", - Host: "ffmpeg.org", - Path: "", - }) - - licenseLink := widget.NewHyperlink(v.app.GetLocalizerService().GetMessage("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) getAboutFfprobe(version string) *fyne.Container { - programmName := canvas.NewText(" FFprobe", colornames.Darkgreen) - programmName.TextStyle = fyne.TextStyle{Bold: true} - programmName.TextSize = 20 - - programmLink := widget.NewHyperlink(v.app.GetLocalizerService().GetMessage("programmLink"), &url.URL{ - Scheme: "https", - Host: "ffmpeg.org", - Path: "ffprobe.html", - }) - - licenseLink := widget.NewHyperlink(v.app.GetLocalizerService().GetMessage("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) 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("programmLink"), &url.URL{ - Scheme: "https", - Host: "ffmpeg.org", - Path: "ffplay.html", - }) - - licenseLink := widget.NewHyperlink(v.app.GetLocalizerService().GetMessage("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), - - container.NewHBox(widget.NewHyperlink("fyne.io/fyne/v2", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "fyne-io/fyne", - })), - container.NewHBox(widget.NewHyperlink("BSD 3-Clause License", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "fyne-io/fyne/blob/master/LICENSE", - })), - widget.NewRichTextFromMarkdown("Copyright (C) 2018 Fyne.io developers (see [AUTHORS](https://github.com/fyne-io/fyne/blob/master/AUTHORS))"), - canvas.NewLine(colornames.Darkgreen), - - container.NewHBox(widget.NewHyperlink("fyne.io/systray", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "fyne-io/systray", - })), - container.NewHBox(widget.NewHyperlink("Apache License", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "fyne-io/systray/blob/master/LICENSE", - })), - canvas.NewLine(colornames.Darkgreen), - - container.NewHBox(widget.NewHyperlink("github.com/BurntSushi/toml", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "BurntSushi/toml", - })), - container.NewHBox(widget.NewHyperlink("The MIT License (MIT)", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "BurntSushi/toml/blob/master/COPYING", - })), - widget.NewLabel("Copyright (c) 2013 TOML authors"), - canvas.NewLine(colornames.Darkgreen), - - container.NewHBox(widget.NewHyperlink("github.com/davecgh/go-spew", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "davecgh/go-spew", - })), - container.NewHBox(widget.NewHyperlink("ISC License", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "davecgh/go-spew/blob/master/LICENSE", - })), - widget.NewLabel("Copyright (c) 2012-2016 Dave Collins "), - canvas.NewLine(colornames.Darkgreen), - - container.NewHBox(widget.NewHyperlink("github.com/fredbi/uri", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "fredbi/uri", - })), - container.NewHBox(widget.NewHyperlink("The MIT License (MIT)", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "fredbi/uri/blob/master/LICENSE.md", - })), - widget.NewLabel("Copyright (c) 2018 Frederic Bidon"), - widget.NewLabel("Copyright (c) 2015 Trey Tacon"), - canvas.NewLine(colornames.Darkgreen), - - container.NewHBox(widget.NewHyperlink("github.com/fsnotify/fsnotify", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "fsnotify/fsnotify", - })), - container.NewHBox(widget.NewHyperlink("BSD-3-Clause license", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "fsnotify/fsnotify/blob/main/LICENSE", - })), - widget.NewLabel("Copyright © 2012 The Go Authors. All rights reserved."), - widget.NewLabel("Copyright © fsnotify Authors. All rights reserved."), - canvas.NewLine(colornames.Darkgreen), - - container.NewHBox(widget.NewHyperlink("github.com/fyne-io/gl-js", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "fyne-io/gl-js", - })), - container.NewHBox(widget.NewHyperlink("BSD-3-Clause license", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "fyne-io/gl-js/blob/master/LICENSE", - })), - widget.NewLabel("Copyright (c) 2009 The Go Authors. All rights reserved."), - canvas.NewLine(colornames.Darkgreen), - - container.NewHBox(widget.NewHyperlink("github.com/fyne-io/glfw-js", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "fyne-io/glfw-js", - })), - container.NewHBox(widget.NewHyperlink("MIT License", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "fyne-io/glfw-js/blob/master/LICENSE", - })), - widget.NewLabel("Copyright (c) 2014 Dmitri Shuralyov"), - canvas.NewLine(colornames.Darkgreen), - - container.NewHBox(widget.NewHyperlink("github.com/fyne-io/image", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "fyne-io/image", - })), - container.NewHBox(widget.NewHyperlink("BSD 3-Clause License", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "fyne-io/image/blob/main/LICENSE", - })), - widget.NewLabel("Copyright (c) 2022, Fyne.io"), - canvas.NewLine(colornames.Darkgreen), - - container.NewHBox(widget.NewHyperlink("github.com/fyne-io/oksvg", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "fyne-io/oksvg", - })), - container.NewHBox(widget.NewHyperlink("BSD 3-Clause License", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "fyne-io/oksvg/blob/master/LICENSE", - })), - widget.NewLabel("Copyright (c) 2018, Steven R Wiley. All rights reserved."), - canvas.NewLine(colornames.Darkgreen), - - container.NewHBox(widget.NewHyperlink("github.com/go-gl/gl", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "go-gl/gl", - })), - container.NewHBox(widget.NewHyperlink("The MIT License (MIT)", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "go-gl/gl/blob/master/LICENSE", - })), - widget.NewLabel("Copyright (c) 2014 Eric Woroshow"), - canvas.NewLine(colornames.Darkgreen), - - container.NewHBox(widget.NewHyperlink("github.com/go-gl/glfw/v3.3/glfw", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "go-gl/glfw/", - })), - container.NewHBox(widget.NewHyperlink("BSD-3-Clause license", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "go-gl/glfw/blob/master/LICENSE", - })), - widget.NewLabel("Copyright (c) 2012 The glfw3-go Authors. All rights reserved."), - canvas.NewLine(colornames.Darkgreen), - - container.NewHBox(widget.NewHyperlink("github.com/go-text/render", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "go-text/render", - })), - container.NewHBox(widget.NewHyperlink("Unlicense OR BSD-3-Clause", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "go-text/render/blob/main/LICENSE", - })), - widget.NewLabel("Copyright 2021 The go-text authors"), - canvas.NewLine(colornames.Darkgreen), - - container.NewHBox(widget.NewHyperlink("github.com/go-text/typesetting", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "go-text/typesetting", - })), - container.NewHBox(widget.NewHyperlink("Unlicense OR BSD-3-Clause", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "go-text/typesetting/blob/main/LICENSE", - })), - widget.NewLabel("Copyright 2021 The go-text authors"), - canvas.NewLine(colornames.Darkgreen), - - container.NewHBox(widget.NewHyperlink("github.com/godbus/dbus/v5", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "godbus/dbus", - })), - container.NewHBox(widget.NewHyperlink("BSD 2-Clause \"Simplified\" License", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "godbus/dbus/blob/master/LICENSE", - })), - widget.NewLabel("Copyright (c) 2013, Georg Reinke (), Google. All rights reserved."), - canvas.NewLine(colornames.Darkgreen), - - container.NewHBox(widget.NewHyperlink("github.com/hack-pad/go-indexeddb", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "hack-pad/go-indexeddb", - })), - container.NewHBox(widget.NewHyperlink("Apache License 2.0", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "hack-pad/go-indexeddb/blob/main/LICENSE", - })), - canvas.NewLine(colornames.Darkgreen), - - container.NewHBox(widget.NewHyperlink("github.com/hack-pad/safejs", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "hack-pad/safejs", - })), - container.NewHBox(widget.NewHyperlink("Apache License 2.0", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "hack-pad/safejs/blob/main/LICENSE", - })), - canvas.NewLine(colornames.Darkgreen), - - container.NewHBox(widget.NewHyperlink("github.com/jeandeaual/go-locale", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "jeandeaual/go-locale", - })), - container.NewHBox(widget.NewHyperlink("MIT License", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "jeandeaual/go-locale/blob/master/LICENSE", - })), - widget.NewLabel("Copyright (c) 2020 Alexis Jeandeau"), - canvas.NewLine(colornames.Darkgreen), - - container.NewHBox(widget.NewHyperlink("github.com/jsummers/gobmp", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "jsummers/gobmp", - })), - container.NewHBox(widget.NewHyperlink("The MIT License (MIT)", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "jsummers/gobmp/blob/master/COPYING.txt", - })), - widget.NewLabel("Copyright (c) 2012-2015 Jason Summers"), - canvas.NewLine(colornames.Darkgreen), - - container.NewHBox(widget.NewHyperlink("github.com/nfnt/resize", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "nfnt/resize", - })), - container.NewHBox(widget.NewHyperlink("ISC License", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "nfnt/resize/blob/master/LICENSE", - })), - widget.NewLabel("Copyright (c) 2012, Jan Schlicht "), - canvas.NewLine(colornames.Darkgreen), - - container.NewHBox(widget.NewHyperlink("github.com/nicksnyder/go-i18n/v2", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "nicksnyder/go-i18n", - })), - container.NewHBox(widget.NewHyperlink("The MIT License (MIT)", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "nicksnyder/go-i18n/blob/main/LICENSE", - })), - widget.NewRichTextFromMarkdown("Copyright (c) 2014 Nick Snyder [https://github.com/nicksnyder](https://github.com/nicksnyder)"), - canvas.NewLine(colornames.Darkgreen), - - container.NewHBox(widget.NewHyperlink("github.com/pmezard/go-difflib", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "pmezard/go-difflib", - })), - container.NewHBox(widget.NewHyperlink("License", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "pmezard/go-difflib/blob/master/LICENSE", - })), - widget.NewLabel("Copyright (c) 2013, Patrick Mezard. All rights reserved."), - canvas.NewLine(colornames.Darkgreen), - - container.NewHBox(widget.NewHyperlink("github.com/rymdport/portal", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "rymdport/portal", - })), - container.NewHBox(widget.NewHyperlink("Apache License 2.0", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "rymdport/portal/blob/main/LICENSE", - })), - canvas.NewLine(colornames.Darkgreen), - - container.NewHBox(widget.NewHyperlink("github.com/srwiley/oksvg", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "srwiley/oksvg", - })), - container.NewHBox(widget.NewHyperlink("BSD 3-Clause License", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "srwiley/oksvg/blob/master/LICENSE", - })), - widget.NewLabel("Copyright (c) 2018, Steven R Wiley. All rights reserved."), - canvas.NewLine(colornames.Darkgreen), - - container.NewHBox(widget.NewHyperlink("github.com/srwiley/rasterx", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "srwiley/rasterx", - })), - container.NewHBox(widget.NewHyperlink("BSD 3-Clause License", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "srwiley/rasterx/blob/master/LICENSE", - })), - widget.NewLabel("Copyright (c) 2018, Steven R Wiley. All rights reserved."), - canvas.NewLine(colornames.Darkgreen), - - container.NewHBox(widget.NewHyperlink("github.com/stretchr/testify", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "stretchr/testify", - })), - container.NewHBox(widget.NewHyperlink("MIT License", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "stretchr/testify/blob/master/LICENSE", - })), - widget.NewLabel("Copyright (c) 2012-2020 Mat Ryer, Tyler Bunnell and contributors."), - canvas.NewLine(colornames.Darkgreen), - - container.NewHBox(widget.NewHyperlink("github.com/ulikunitz/xz", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "ulikunitz/xz", - })), - container.NewHBox(widget.NewHyperlink("License", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "ulikunitz/xz/blob/master/LICENSE", - })), - widget.NewLabel("Copyright (c) 2014-2022 Ulrich Kunitz. All rights reserved."), - canvas.NewLine(colornames.Darkgreen), - - container.NewHBox(widget.NewHyperlink("github.com/yuin/goldmark", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "yuin/goldmark", - })), - container.NewHBox(widget.NewHyperlink("MIT License", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "yuin/goldmark/blob/master/LICENSE", - })), - widget.NewLabel("Copyright (c) 2019 Yusuke Inuzuka"), - canvas.NewLine(colornames.Darkgreen), - - container.NewHBox(widget.NewHyperlink("go.etcd.io/bbolt", &url.URL{ - Scheme: "https", - Host: "pkg.go.dev", - Path: "go.etcd.io/bbolt", - })), - container.NewHBox(widget.NewHyperlink("MIT License", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "etcd-io/bbolt/blob/main/LICENSE", - })), - widget.NewLabel("Copyright (c) 2013 Ben Johnson"), - canvas.NewLine(colornames.Darkgreen), - - container.NewHBox(widget.NewHyperlink("golang.org/x/image", &url.URL{ - Scheme: "https", - Host: "pkg.go.dev", - Path: "golang.org/x/image", - })), - container.NewHBox(widget.NewHyperlink("License", &url.URL{ - Scheme: "https", - Host: "cs.opensource.google", - Path: "go/x/image/+/master:LICENSE", - })), - widget.NewLabel("Copyright 2009 The Go Authors."), - canvas.NewLine(colornames.Darkgreen), - - container.NewHBox(widget.NewHyperlink("golang.org/x/net", &url.URL{ - Scheme: "https", - Host: "pkg.go.dev", - Path: "golang.org/x/net", - })), - container.NewHBox(widget.NewHyperlink("License", &url.URL{ - Scheme: "https", - Host: "cs.opensource.google", - Path: "go/x/net/+/master:LICENSE", - })), - widget.NewLabel("Copyright 2009 The Go Authors."), - canvas.NewLine(colornames.Darkgreen), - - container.NewHBox(widget.NewHyperlink("golang.org/x/sys", &url.URL{ - Scheme: "https", - Host: "pkg.go.dev", - Path: "golang.org/x/sys", - })), - container.NewHBox(widget.NewHyperlink("License", &url.URL{ - Scheme: "https", - Host: "cs.opensource.google", - Path: "go/x/sys/+/master:LICENSE", - })), - widget.NewLabel("Copyright 2009 The Go Authors."), - canvas.NewLine(colornames.Darkgreen), - - container.NewHBox(widget.NewHyperlink("golang.org/x/text", &url.URL{ - Scheme: "https", - Host: "pkg.go.dev", - Path: "golang.org/x/text", - })), - container.NewHBox(widget.NewHyperlink("License", &url.URL{ - Scheme: "https", - Host: "cs.opensource.google", - Path: "go/x/text/+/master:LICENSE", - })), - widget.NewLabel("Copyright 2009 The Go Authors."), - canvas.NewLine(colornames.Darkgreen), - - container.NewHBox(widget.NewHyperlink("gopkg.in/yaml.v3", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "go-yaml/yaml/tree/v3.0.1", - })), - container.NewHBox(widget.NewHyperlink("MIT License and Apache License 2.0", &url.URL{ - Scheme: "http", - Host: "github.com", - Path: "go-yaml/yaml/blob/v3.0.1/LICENSE", - })), - widget.NewLabel("Copyright (c) 2006-2010 Kirill Simonov"), - widget.NewLabel("Copyright (c) 2006-2011 Kirill Simonov"), - widget.NewLabel("Copyright (c) 2011-2019 Canonical Ltd"), - canvas.NewLine(colornames.Darkgreen), - - container.NewHBox(widget.NewHyperlink("github.com/golang/go", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "golang/go", - })), - container.NewHBox(widget.NewHyperlink("BSD 3-Clause \"New\" or \"Revised\" License", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "golang/go/blob/master/LICENSE", - })), - widget.NewLabel("Copyright 2009 The Go Authors."), - canvas.NewLine(colornames.Darkgreen), - ) -} diff --git a/menu/view_setting.go b/menu/view_setting.go deleted file mode 100644 index 02e7fbd..0000000 --- a/menu/view_setting.go +++ /dev/null @@ -1,101 +0,0 @@ -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" - "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(), - 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().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("menuSettingsLanguage"), - Widget: selectLanguage, - }, - { - Text: v.app.GetLocalizerService().GetMessage("menuSettingsTheme"), - Widget: selectTheme, - }, - { - Widget: errorMessage, - }, - }, - SubmitText: v.app.GetLocalizerService().GetMessage("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("cancel") - } - - messageHead := v.app.GetLocalizerService().GetMessage("settings") - v.app.GetWindow().SetContent(widget.NewCard(messageHead, "", form)) -} diff --git a/setting/directory_for_saving.go b/setting/directory_for_saving.go deleted file mode 100644 index c77fde5..0000000 --- a/setting/directory_for_saving.go +++ /dev/null @@ -1,22 +0,0 @@ -package setting - -type DirectoryForSavingContract interface { - GetDirectoryForSaving() string - SaveDirectoryForSaving(path string) Setting -} - -type DirectoryForSaving struct { - settingRepository RepositoryContract -} - -func NewSettingDirectoryForSaving(settingRepository RepositoryContract) *DirectoryForSaving { - return &DirectoryForSaving{settingRepository: settingRepository} -} - -func (setting DirectoryForSaving) GetDirectoryForSaving() string { - return setting.settingRepository.GetValue("directoryForSaving") -} - -func (setting DirectoryForSaving) SaveDirectoryForSaving(path string) Setting { - return setting.settingRepository.CreateOrUpdate("directoryForSaving", path) -} diff --git a/setting/entity.go b/setting/entity.go deleted file mode 100644 index d0c8384..0000000 --- a/setting/entity.go +++ /dev/null @@ -1,6 +0,0 @@ -package setting - -type Setting struct { - Code string - Value string -} diff --git a/setting/repository.go b/setting/repository.go deleted file mode 100644 index 9365670..0000000 --- a/setting/repository.go +++ /dev/null @@ -1,39 +0,0 @@ -package setting - -import ( - "fyne.io/fyne/v2" -) - -type RepositoryContract interface { - Create(setting Setting) Setting - CreateOrUpdate(code string, value string) Setting - GetValue(code string) string -} - -type Repository struct { - app fyne.App -} - -func NewRepository(app fyne.App) *Repository { - return &Repository{ - app: app, - } -} - -func (r Repository) GetValue(code string) string { - return r.app.Preferences().String(code) -} - -func (r Repository) Create(setting Setting) Setting { - r.app.Preferences().SetString(setting.Code, setting.Value) - return setting -} - -func (r Repository) CreateOrUpdate(code string, value string) Setting { - var setting Setting - setting.Code = code - setting.Value = value - - r.app.Preferences().SetString(code, value) - return setting -} diff --git a/theme/repository.go b/theme/repository.go deleted file mode 100644 index 797527b..0000000 --- a/theme/repository.go +++ /dev/null @@ -1,28 +0,0 @@ -package theme - -import "git.kor-elf.net/kor-elf/gui-for-ffmpeg/setting" - -type RepositoryContract interface { - GetCode() string - Save(code string) setting.Setting -} - -type Repository struct { - settingRepository setting.RepositoryContract -} - -func NewRepository(settingRepository setting.RepositoryContract) *Repository { - return &Repository{settingRepository: settingRepository} -} - -func (r Repository) GetCode() string { - name := r.settingRepository.GetValue("theme") - if len(name) == 0 { - return "default" - } - return name -} - -func (r Repository) Save(code string) setting.Setting { - return r.settingRepository.CreateOrUpdate("theme", code) -} diff --git a/theme/theme.go b/theme/theme.go deleted file mode 100644 index 1b4aa8b..0000000 --- a/theme/theme.go +++ /dev/null @@ -1,148 +0,0 @@ -package theme - -import ( - "fyne.io/fyne/v2" - fyneTheme "fyne.io/fyne/v2/theme" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" - "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 { - _ = t.repository.Save(themeInfo.GetName()) - - 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("themesNameDefault"), - } - - themesNameLight := &themeInfo{ - name: "light", - title: localizer.GetMessage("themesNameLight"), - variant: fyneTheme.VariantLight, - } - - themesNameDark := &themeInfo{ - name: "dark", - title: localizer.GetMessage("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) -} -- 2.47.2 From 7930a907f1ff2e0461146bb8f0deccd5eb7720dc Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Tue, 10 Jun 2025 22:25:21 +0500 Subject: [PATCH 31/41] Removed unused `AppMetadata` configuration and refactored imports in `main.go`. Because the data is duplicated with FyneApp.toml. --- main.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/main.go b/main.go index 4455f47..41749c5 100644 --- a/main.go +++ b/main.go @@ -1,24 +1,15 @@ package main import ( - "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application/convertor" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/application/setting" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/controller" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/ffmpeg" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/internal/resources" ) func main() { - appMetadata := fyne.AppMetadata{ - ID: "net.kor-elf.projects.gui-for-ffmpeg", - Name: "GUI for FFmpeg", - Version: "0.9.0", - Icon: resources.IconAppLogoResource(), - } - app.SetMetadata(appMetadata) fyneApp := app.New() appSetting := setting.NewSetting(fyneApp) ffmpegService := ffmpeg.NewUtilities(appSetting) -- 2.47.2 From 0a22377cd6a965d8b9bd840c371ebad3b9ed8085 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Tue, 10 Jun 2025 23:41:37 +0500 Subject: [PATCH 32/41] Removed references to `bbolt` and its license info from "About" screen and `LICENSE-3RD-PARTY.txt` as it's no longer used. Added FFmpeg license details to `LICENSE-3RD-PARTY.txt`. --- LICENSE-3RD-PARTY.txt | 743 +++++++++++++++++++++++++++++++++---- internal/gui/view/about.go | 13 - 2 files changed, 681 insertions(+), 75 deletions(-) diff --git a/LICENSE-3RD-PARTY.txt b/LICENSE-3RD-PARTY.txt index 548dda4..479031f 100644 --- a/LICENSE-3RD-PARTY.txt +++ b/LICENSE-3RD-PARTY.txt @@ -1566,31 +1566,6 @@ SOFTWARE. -------------------------------------------------------------------------------- -go.etcd.io/bbolt - -The MIT License (MIT) - -Copyright (c) 2013 Ben Johnson - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - --------------------------------------------------------------------------------- - golang.org/x/image Copyright 2009 The Go Authors. @@ -1721,43 +1696,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. gopkg.in/yaml.v3 -This project is covered by two different licenses: MIT and Apache. - -#### MIT License #### - -The following files were ported to Go from C files of libyaml, and thus -are still covered by their original MIT license, with the additional -copyright staring in 2011 when the project was ported over: - - apic.go emitterc.go parserc.go readerc.go scannerc.go - writerc.go yamlh.go yamlprivateh.go - -Copyright (c) 2006-2010 Kirill Simonov -Copyright (c) 2006-2011 Kirill Simonov - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -### Apache License ### - -All the remaining project files are covered by the Apache license: - -Copyright (c) 2011-2019 Canonical Ltd +Copyright 2011-2016 Canonical Ltd. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -1805,3 +1744,683 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- +FFmpeg + +FFmpeg is a trademark of Fabrice Bellard, originator of the FFmpeg project. +https://ffmpeg.org/legal.html + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. + diff --git a/internal/gui/view/about.go b/internal/gui/view/about.go index 5744bad..a92fe73 100644 --- a/internal/gui/view/about.go +++ b/internal/gui/view/about.go @@ -524,19 +524,6 @@ func license3RDParty() *fyne.Container { widget.NewLabel("Copyright (c) 2019 Yusuke Inuzuka"), canvas.NewLine(colornames.Darkgreen), - container.NewHBox(widget.NewHyperlink("go.etcd.io/bbolt", &url.URL{ - Scheme: "https", - Host: "pkg.go.dev", - Path: "go.etcd.io/bbolt", - })), - container.NewHBox(widget.NewHyperlink("MIT License", &url.URL{ - Scheme: "https", - Host: "github.com", - Path: "etcd-io/bbolt/blob/main/LICENSE", - })), - widget.NewLabel("Copyright (c) 2013 Ben Johnson"), - canvas.NewLine(colornames.Darkgreen), - container.NewHBox(widget.NewHyperlink("golang.org/x/image", &url.URL{ Scheme: "https", Host: "pkg.go.dev", -- 2.47.2 From c59c87d109291b3aaf8128d690799a76299f2c13 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sat, 14 Jun 2025 19:30:14 +0500 Subject: [PATCH 33/41] Duplicated: `icon.png` and `screenshot-gui-for-ffmpeg.png` in assets, so that I could later delete them from their current locations after the links to the images have changed everywhere. --- assets/icon.png | Bin 0 -> 29642 bytes assets/screenshot-gui-for-ffmpeg.png | Bin 0 -> 78653 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 assets/icon.png create mode 100644 assets/screenshot-gui-for-ffmpeg.png diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6eaf8f62869d7c2c6d0313a8f11557fdb18cfee2 GIT binary patch literal 29642 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

E{^L`OzEE8C7HTRo(@;!IeU5+azqK2l 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 literal 0 HcmV?d00001 -- 2.47.2 From 57767de4b3f9a401214bf0d0c48e4afd36cb177e Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sat, 14 Jun 2025 19:32:05 +0500 Subject: [PATCH 34/41] Added files that will be copied during the build of the application for Linux. --- dist/linux/Makefile | 39 +++++++++++++++++++ dist/linux/Readme-eng.txt | 7 ++++ dist/linux/Readme-rus.txt | 7 ++++ .../share/applications/gui-for-ffmpeg.desktop | 9 +++++ 4 files changed, 62 insertions(+) create mode 100644 dist/linux/Makefile create mode 100644 dist/linux/Readme-eng.txt create mode 100644 dist/linux/Readme-rus.txt create mode 100644 dist/linux/usr/local/share/applications/gui-for-ffmpeg.desktop diff --git a/dist/linux/Makefile b/dist/linux/Makefile new file mode 100644 index 0000000..f8ba5e5 --- /dev/null +++ b/dist/linux/Makefile @@ -0,0 +1,39 @@ +# If PREFIX isn't provided, we check for $(DESTDIR)/usr/local and use that if it exists. +# Otherwice we fall back to using /usr. + +LOCAL != test -d $(DESTDIR)/usr/local && echo -n "/local" || echo -n "" +LOCAL ?= $(shell test -d $(DESTDIR)/usr/local && echo "/local" || echo "") +PREFIX ?= /usr$(LOCAL) + +Name := "gui-for-ffmpeg" +Exec := "gui-for-ffmpeg" +Icon := "gui-for-ffmpeg.png" + +default: + # User install + # Run "make user-install" to install in ~/.local/ + # Run "make user-uninstall" to uninstall from ~/.local/ + # + # System install + # Run "sudo make install" to install the application. + # Run "sudo make uninstall" to uninstall the application. + +install: + install -Dm00644 usr/local/share/applications/$(Name).desktop $(DESTDIR)$(PREFIX)/share/applications/$(Name).desktop + install -Dm00755 usr/local/bin/$(Exec) $(DESTDIR)$(PREFIX)/bin/$(Exec) + install -Dm00644 usr/local/share/pixmaps/$(Icon) $(DESTDIR)$(PREFIX)/share/pixmaps/$(Icon) +uninstall: + -rm $(DESTDIR)$(PREFIX)/share/applications/$(Name).desktop + -rm $(DESTDIR)$(PREFIX)/bin/$(Exec) + -rm $(DESTDIR)$(PREFIX)/share/pixmaps/$(Icon) + +user-install: + install -Dm00644 usr/local/share/applications/$(Name).desktop $(DESTDIR)$(HOME)/.local/share/applications/$(Name).desktop + install -Dm00755 usr/local/bin/$(Exec) $(DESTDIR)$(HOME)/.local/bin/$(Exec) + install -Dm00644 usr/local/share/pixmaps/$(Icon) $(DESTDIR)$(HOME)/.local/share/icons/$(Icon) + sed -i -e "s,Exec=$(Exec),Exec=$(DESTDIR)$(HOME)/.local/bin/$(Exec),g" $(DESTDIR)$(HOME)/.local/share/applications/$(Name).desktop + +user-uninstall: + -rm $(DESTDIR)$(HOME)/.local/share/applications/$(Name).desktop + -rm $(DESTDIR)$(HOME)/.local/bin/$(Exec) + -rm $(DESTDIR)$(HOME)/.local/share/icons/$(Icon) diff --git a/dist/linux/Readme-eng.txt b/dist/linux/Readme-eng.txt new file mode 100644 index 0000000..0548615 --- /dev/null +++ b/dist/linux/Readme-eng.txt @@ -0,0 +1,7 @@ +User install +Run "make user-install" to install in ~/.local/ +Run "make user-uninstall" to uninstall from ~/.local/ + +System install +Run "sudo make install" to install the application. +Run "sudo make uninstall" to uninstall the application. diff --git a/dist/linux/Readme-rus.txt b/dist/linux/Readme-rus.txt new file mode 100644 index 0000000..9331b2b --- /dev/null +++ b/dist/linux/Readme-rus.txt @@ -0,0 +1,7 @@ +Установить для пользователя (рекомендуется) +Запустите "make user-install" для установки в домашнюю папку ~/.local/ +Запустите "make user-uninstall" для удаления из домашней папки ~/.local/ + +Установить для всей системы +Запустить "sudo make install" Для установки в систему. +Запустить "sudo make uninstall" Для удаления из системы. diff --git a/dist/linux/usr/local/share/applications/gui-for-ffmpeg.desktop b/dist/linux/usr/local/share/applications/gui-for-ffmpeg.desktop new file mode 100644 index 0000000..a687360 --- /dev/null +++ b/dist/linux/usr/local/share/applications/gui-for-ffmpeg.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Type=Application +Name=gui-for-ffmpeg +GenericName=GUI for FFmpeg +Exec=gui-for-ffmpeg +Icon=gui-for-ffmpeg +Comment=A simple interface for the FFmpeg console utility. +Categories=AudioVideo;Utility; +Keywords=ffmpeg;media;convert;transcode;audio;video;конвертер;видео;аудио;кодек; -- 2.47.2 From 63c13de181f117fcc154ee0b83aaa4dd86984ee8 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sat, 14 Jun 2025 19:32:29 +0500 Subject: [PATCH 35/41] Add Makefile to automate build process for Linux and Windows --- Makefile | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..fcf22ab --- /dev/null +++ b/Makefile @@ -0,0 +1,88 @@ +VERSION ?= $(shell grep '^ *Version *= *' FyneApp.toml | sed -E "s/.*=[[:space:]]*\"([0-9\.]+)\".*/\1/") + +BUILD_TMP := fyne-cross/tmp + +WINDOWS_AMD64 := gui-for-ffmpeg-$(VERSION)-windows-amd64 +BUILD_TMP_WINDOWS_AMD64 := $(BUILD_TMP)/$(WINDOWS_AMD64) + +LINUX_AMD64 := gui-for-ffmpeg-$(VERSION)-linux-amd64 +BUILD_TMP_LINUX_AMD64 := $(BUILD_TMP)/$(LINUX_AMD64) + +RELEASES := fyne-cross/releases/$(VERSION) + +default: + # Run "make build-for-linux_amd64" + # Run "make build-for-windows_amd64" + # Build for all + # Run "make build" + +build: + make build-for-linux_amd64 + make build-for-windows_amd64 + # $(RELEASES)/$(LINUX_AMD64).tar.gz + # $(RELEASES)/$(LINUX_AMD64).tar.gz.sha256 + # $(RELEASES)/$(WINDOWS_AMD64).zip + # $(RELEASES)/$(WINDOWS_AMD64).zip.sha256 + +build-for-windows_amd64: + fyne-cross windows --icon "assets/icon.png" --app-id "net.kor-elf.projects.gui-for-ffmpeg" --app-version $(VERSION) -name "gui-for-ffmpeg" + + @if [ -d $(BUILD_TMP_WINDOWS_AMD64) ]; then \ + rm -rf $(BUILD_TMP_WINDOWS_AMD64)/*; \ + else \ + mkdir -p $(BUILD_TMP_WINDOWS_AMD64); \ + fi + cp LICENSE $(BUILD_TMP_WINDOWS_AMD64)/LICENSE + cp LICENSE-3RD-PARTY.txt $(BUILD_TMP_WINDOWS_AMD64)/LICENSE-3RD-PARTY.txt + cp fyne-cross/bin/windows-amd64/gui-for-ffmpeg.exe $(BUILD_TMP_WINDOWS_AMD64)/gui-for-ffmpeg.exe + cd $(BUILD_TMP) && 7z a -tzip $(WINDOWS_AMD64).zip $(WINDOWS_AMD64) + + @if [ ! -d $(RELEASES) ]; then \ + mkdir -p $(RELEASES); \ + fi + + @if [ -f $(RELEASES)/$(WINDOWS_AMD64).zip ]; then \ + rm $(RELEASES)/$(WINDOWS_AMD64).zip; \ + fi + + @if [ -f $(RELEASES)/$(WINDOWS_AMD64).zip.sha256 ]; then \ + rm $(RELEASES)/$(WINDOWS_AMD64).zip.sha256; \ + fi + + mv $(BUILD_TMP)/$(WINDOWS_AMD64).zip $(RELEASES)/$(WINDOWS_AMD64).zip + cd $(RELEASES) && sha256sum $(WINDOWS_AMD64).zip > $(WINDOWS_AMD64).zip.sha256 + # $(RELEASES)/$(WINDOWS_AMD64).zip + # $(RELEASES)/$(WINDOWS_AMD64).zip.sha256 + +build-for-linux_amd64: + fyne-cross linux --icon "assets/icon.png" --app-id "net.kor-elf.projects.gui-for-ffmpeg" --app-version $(VERSION) -name "gui-for-ffmpeg" + + @if [ -d $(BUILD_TMP_LINUX_AMD64) ]; then \ + rm -rf $(BUILD_TMP_LINUX_AMD64)/*; \ + else \ + mkdir -p $(BUILD_TMP_LINUX_AMD64); \ + fi + cp -r dist/linux/* $(BUILD_TMP_LINUX_AMD64)/ + cp LICENSE $(BUILD_TMP_LINUX_AMD64)/LICENSE + cp LICENSE-3RD-PARTY.txt $(BUILD_TMP_LINUX_AMD64)/LICENSE-3RD-PARTY.txt + cp fyne-cross/bin/linux-amd64/gui-for-ffmpeg $(BUILD_TMP_LINUX_AMD64)/usr/local/bin/gui-for-ffmpeg + cp assets/icon.png $(BUILD_TMP_LINUX_AMD64)/usr/local/share/pixmaps/gui-for-ffmpeg.png + + cd $(BUILD_TMP) && tar -czvf $(LINUX_AMD64).tar.gz $(LINUX_AMD64) + + @if [ ! -d $(RELEASES) ]; then \ + mkdir -p $(RELEASES); \ + fi + + @if [ -f $(RELEASES)/$(LINUX_AMD64).tar.gz ]; then \ + rm $(RELEASES)/$(LINUX_AMD64).tar.gz; \ + fi + + @if [ -f $(RELEASES)/$(LINUX_AMD64).tar.gz.sha256 ]; then \ + rm $(RELEASES)/$(LINUX_AMD64).tar.gz.sha256; \ + fi + + mv $(BUILD_TMP)/$(LINUX_AMD64).tar.gz $(RELEASES)/$(LINUX_AMD64).tar.gz + cd $(RELEASES) && sha256sum $(LINUX_AMD64).tar.gz > $(LINUX_AMD64).tar.gz.sha256 + # $(RELEASES)/$(LINUX_AMD64).tar.gz + # $(RELEASES)/$(LINUX_AMD64).tar.gz.sha256 -- 2.47.2 From c45c106f2f2f84a7abd231c508d22aee66dff116 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sat, 14 Jun 2025 19:34:47 +0500 Subject: [PATCH 36/41] Updated README for link fixes, new Makefile build instructions, and adjusted folder structure documentation. Deleted obsolete folder structure screenshot. --- README.md | 28 ++++++++++++++++--------- images/screenshot-folder-structure.png | Bin 5659 -> 0 bytes 2 files changed, 18 insertions(+), 10 deletions(-) delete mode 100644 images/screenshot-folder-structure.png diff --git a/README.md b/README.md index 23180b8..ac36200 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # GUI for FFmpeg

Простенький интерфейс для консольной утилиты FFmpeg. Но я не являюсь автором самой утилиты FFmpeg.

-

FFmpeg — торговая марка Fabrice Bellard, создателя проекта FFmpeg.

+

FFmpeg — торговая марка Fabrice Bellard, создателя проекта FFmpeg.

Программное обеспечение является MIT (см. LICENSE) и использует сторонние библиотеки, которые распространяются на их собственных условиях (см. LICENSE-3RD-PARTY.txt).

- +Скриншот программы

Скачать скомпилированные готовые версии можно тут: https://git.kor-elf.net/kor-elf/gui-for-ffmpeg/releases.

@@ -13,6 +13,18 @@ 1. go install fyne.io/fyne/v2/cmd/fyne@latest 2. fyne get git.kor-elf.net/kor-elf/gui-for-ffmpeg +## Скомпилировать через Makefile: +1. git clone https://git.kor-elf.net/kor-elf/gui-for-ffmpeg.git +2. Переходим в папку проекта и там переходим в папку src: **cd gui-for-ffmpeg** +3. Ознакамливаемся, что нужно ещё установить для Вашей ОС для простого запуска (через go run) тут: https://docs.fyne.io/started/ +4. go install github.com/fyne-io/fyne-cross@latest + * У Вас так же должен быть установлен docker + * О fyne-cross можно по подробней почитать тут: https://github.com/fyne-io/fyne-cross +5. * make build-for-linux_amd64 + * make build-for-windows_amd64 + * Или просто **make build** +6. Создаться папка с архивом в **fyne-cross/releases** + ## Скомпилировать через исходники: 1. git clone https://git.kor-elf.net/kor-elf/gui-for-ffmpeg.git 2. Переходим в папку проекта и там переходим в папку src: **cd gui-for-ffmpeg** @@ -21,11 +33,7 @@ 5. go install github.com/fyne-io/fyne-cross@latest * У Вас так же должен быть установлен docker * О fyne-cross можно по подробней почитать тут: https://github.com/fyne-io/fyne-cross -6. * fyne-cross windows --icon icon.png --app-id "." -name "gui-for-ffmpeg" - * fyne-cross linux --icon icon.png --app-id "." -name "gui-for-ffmpeg" -7. Создаться папка **fyne-cross/bin** и там будет созданна папка с тем названием под которую Вы компилировали приложения (linux-amd64 или windows-amd64). -8. В папку **fyne-cross/bin/linux-amd64** или **fyne-cross/bin/windows-amd64** копируете: - * LICENSE - * LICENSE-3RD-PARTY.txt -

Структура должна получиться такая:

- +6. * fyne-cross windows --icon "assets/icon.png" --name "gui-for-ffmpeg" + * fyne-cross linux --icon "assets/icon.png" --name "gui-for-ffmpeg" +7. Создаться папка **fyne-cross/dist** и там будет созданна папка с тем названием под которую Вы компилировали приложения (linux-amd64 или windows-amd64). +8. В папке **fyne-cross/bin/linux-amd64** или **fyne-cross/bin/windows-amd64** будут архивы, которые надо распаковать и пользоваться программой. diff --git a/images/screenshot-folder-structure.png b/images/screenshot-folder-structure.png deleted file mode 100644 index ab90c3614a9a8674697378a8b359abc1f4847895..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5659 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 G=)V9!r5!~8 -- 2.47.2 From c4d205a79ea7a8ed957d03d8ba7570f08c02fb0d Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sat, 14 Jun 2025 19:52:29 +0500 Subject: [PATCH 37/41] Updated `FyneApp.toml` with new `Website`, updated `Icon` path, bumped version to `1.0.0`, added Linux/BSD-specific fields (GenericName, Categories, Comment, Keywords). --- FyneApp.toml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/FyneApp.toml b/FyneApp.toml index b30925b..80050b2 100644 --- a/FyneApp.toml +++ b/FyneApp.toml @@ -1,6 +1,14 @@ +Website = "https://gui-for-ffmpeg.projects.kor-elf.net/language/en" + [Details] - Icon = "icon.png" + Icon = "assets/icon.png" Name = "GUI for FFmpeg" ID = "net.kor-elf.projects.gui-for-ffmpeg" - Version = "0.9.0" - Build = 11 + Version = "1.0.0" + Build = 75 + +[LinuxAndBSD] + GenericName = "GUI for FFmpeg" + Categories = ["AudioVideo", "Utility"] + Comment = "A simple interface for the FFmpeg console utility." + Keywords = ["ffmpeg", "media", "convert", "transcode", "audio", "video", "конвертер", "видео", "аудио", "кодек"] -- 2.47.2 From 7f410ef700d9617e81ecd8aef9db08709df2bb7b Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sat, 14 Jun 2025 21:54:50 +0500 Subject: [PATCH 38/41] Simplified README build instructions by removing redundant `--icon` and `--name` flags from `fyne-cross` commands. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ac36200..cd7e398 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ 5. go install github.com/fyne-io/fyne-cross@latest * У Вас так же должен быть установлен docker * О fyne-cross можно по подробней почитать тут: https://github.com/fyne-io/fyne-cross -6. * fyne-cross windows --icon "assets/icon.png" --name "gui-for-ffmpeg" - * fyne-cross linux --icon "assets/icon.png" --name "gui-for-ffmpeg" +6. * fyne-cross windows + * fyne-cross linux 7. Создаться папка **fyne-cross/dist** и там будет созданна папка с тем названием под которую Вы компилировали приложения (linux-amd64 или windows-amd64). 8. В папке **fyne-cross/bin/linux-amd64** или **fyne-cross/bin/windows-amd64** будут архивы, которые надо распаковать и пользоваться программой. -- 2.47.2 From f6958ffa970989080c616e7e4d150f7c7c5a30d4 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sat, 14 Jun 2025 21:55:30 +0500 Subject: [PATCH 39/41] Standardized application name in `.desktop` file (`Name` field updated to "GUI for FFmpeg"). --- dist/linux/usr/local/share/applications/gui-for-ffmpeg.desktop | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dist/linux/usr/local/share/applications/gui-for-ffmpeg.desktop b/dist/linux/usr/local/share/applications/gui-for-ffmpeg.desktop index a687360..cda0b2b 100644 --- a/dist/linux/usr/local/share/applications/gui-for-ffmpeg.desktop +++ b/dist/linux/usr/local/share/applications/gui-for-ffmpeg.desktop @@ -1,6 +1,6 @@ [Desktop Entry] Type=Application -Name=gui-for-ffmpeg +Name=GUI for FFmpeg GenericName=GUI for FFmpeg Exec=gui-for-ffmpeg Icon=gui-for-ffmpeg -- 2.47.2 From b56199fe8b16d2bf4e243f4a5739dff66eaae788 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sat, 14 Jun 2025 21:55:52 +0500 Subject: [PATCH 40/41] Simplified `fyne-cross` build commands in Makefile by removing redundant flags. Adjusted Windows binary naming during packaging. --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index fcf22ab..564205c 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ build: # $(RELEASES)/$(WINDOWS_AMD64).zip.sha256 build-for-windows_amd64: - fyne-cross windows --icon "assets/icon.png" --app-id "net.kor-elf.projects.gui-for-ffmpeg" --app-version $(VERSION) -name "gui-for-ffmpeg" + fyne-cross windows @if [ -d $(BUILD_TMP_WINDOWS_AMD64) ]; then \ rm -rf $(BUILD_TMP_WINDOWS_AMD64)/*; \ @@ -34,7 +34,7 @@ build-for-windows_amd64: fi cp LICENSE $(BUILD_TMP_WINDOWS_AMD64)/LICENSE cp LICENSE-3RD-PARTY.txt $(BUILD_TMP_WINDOWS_AMD64)/LICENSE-3RD-PARTY.txt - cp fyne-cross/bin/windows-amd64/gui-for-ffmpeg.exe $(BUILD_TMP_WINDOWS_AMD64)/gui-for-ffmpeg.exe + cp "fyne-cross/bin/windows-amd64/GUI for FFmpeg.exe" $(BUILD_TMP_WINDOWS_AMD64)/gui-for-ffmpeg.exe cd $(BUILD_TMP) && 7z a -tzip $(WINDOWS_AMD64).zip $(WINDOWS_AMD64) @if [ ! -d $(RELEASES) ]; then \ @@ -55,7 +55,7 @@ build-for-windows_amd64: # $(RELEASES)/$(WINDOWS_AMD64).zip.sha256 build-for-linux_amd64: - fyne-cross linux --icon "assets/icon.png" --app-id "net.kor-elf.projects.gui-for-ffmpeg" --app-version $(VERSION) -name "gui-for-ffmpeg" + fyne-cross linux @if [ -d $(BUILD_TMP_LINUX_AMD64) ]; then \ rm -rf $(BUILD_TMP_LINUX_AMD64)/*; \ -- 2.47.2 From 26827d5ccdd14e6edd37a0934d126d97d3731f26 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sat, 14 Jun 2025 22:46:00 +0500 Subject: [PATCH 41/41] Window title updated to "GUI for FFmpeg". --- internal/controller/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/controller/main.go b/internal/controller/main.go index 8d75dc9..e6f0e75 100644 --- a/internal/controller/main.go +++ b/internal/controller/main.go @@ -19,7 +19,7 @@ type controller struct { } func NewController(app application.AppContract) ControllerContract { - fyneWindow := app.FyneApp().NewWindow(app.FyneApp().Metadata().Name) + fyneWindow := app.FyneApp().NewWindow("GUI for FFmpeg") fyneWindow.SetMaster() queueLayout := window.NewQueueLayout(app.GetFFmpegService()) app.GetQueueService().AddListener(queueLayout) -- 2.47.2