From c4ec958576aa09b9293308646d5e4dee63edb344 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Mon, 12 Feb 2024 22:21:47 +0600 Subject: [PATCH 1/7] Moved the code from src to the root. --- README.md | 11 +++++------ {src/convertor => convertor}/repository.go | 2 +- {src/convertor => convertor}/service.go | 2 +- {src/convertor => convertor}/view.go | 4 ++-- {src/convertor => convertor}/view_setting.go | 2 +- .../view_setting_button_download_ffmpeg_anyos.go | 0 ...view_setting_button_download_ffmpeg_windows.go | 0 {src/data => data}/.gitignore | 0 {src/error => error}/view.go | 2 +- src/go.mod => go.mod | 2 +- src/go.sum => go.sum | 0 {src/handler => handler}/convertor.go | 6 +++--- {src/handler => handler}/convertor_anyos.go | 2 +- {src/handler => handler}/convertor_windows.go | 0 {src/handler => handler}/main.go | 2 +- {src/handler => handler}/menu.go | 4 ++-- {src/helper => helper}/helper.go | 0 {src/helper => helper}/path_separator.go | 0 {src/helper => helper}/path_separator_window.go | 0 .../prepare_background_command.go | 0 .../prepare_background_command_windows.go | 0 src/icon.png => icon.png | Bin {src/languages => languages}/active.en.toml | 0 {src/languages => languages}/active.kk.toml | 0 {src/languages => languages}/active.ru.toml | 0 {src/languages => languages}/translate.en.toml | 0 {src/languages => languages}/translate.kk.toml | 0 {src/localizer => localizer}/repository.go | 2 +- {src/localizer => localizer}/service.go | 0 {src/localizer => localizer}/view.go | 0 src/main.go => main.go | 14 +++++++------- {src/menu => menu}/view.go | 2 +- {src/migration => migration}/migration.go | 2 +- {src/setting => setting}/entity.go | 0 {src/setting => setting}/repository.go | 0 35 files changed, 29 insertions(+), 30 deletions(-) rename {src/convertor => convertor}/repository.go (94%) rename {src/convertor => convertor}/service.go (98%) rename {src/convertor => convertor}/view.go (98%) rename {src/convertor => convertor}/view_setting.go (98%) rename {src/convertor => convertor}/view_setting_button_download_ffmpeg_anyos.go (100%) rename {src/convertor => convertor}/view_setting_button_download_ffmpeg_windows.go (100%) rename {src/data => data}/.gitignore (100%) rename {src/error => error}/view.go (96%) rename src/go.mod => go.mod (97%) rename src/go.sum => go.sum (100%) rename {src/handler => handler}/convertor.go (97%) rename {src/handler => handler}/convertor_anyos.go (87%) rename {src/handler => handler}/convertor_windows.go (100%) rename {src/handler => handler}/main.go (94%) rename {src/handler => handler}/menu.go (97%) rename {src/helper => helper}/helper.go (100%) rename {src/helper => helper}/path_separator.go (100%) rename {src/helper => helper}/path_separator_window.go (100%) rename {src/helper => helper}/prepare_background_command.go (100%) rename {src/helper => helper}/prepare_background_command_windows.go (100%) rename src/icon.png => icon.png (100%) rename {src/languages => languages}/active.en.toml (100%) rename {src/languages => languages}/active.kk.toml (100%) rename {src/languages => languages}/active.ru.toml (100%) rename {src/languages => languages}/translate.en.toml (100%) rename {src/languages => languages}/translate.kk.toml (100%) rename {src/localizer => localizer}/repository.go (91%) rename {src/localizer => localizer}/service.go (100%) rename {src/localizer => localizer}/view.go (100%) rename src/main.go => main.go (89%) rename {src/menu => menu}/view.go (99%) rename {src/migration => migration}/migration.go (75%) rename {src/setting => setting}/entity.go (100%) rename {src/setting => setting}/repository.go (100%) diff --git a/README.md b/README.md index 769b0cd..7ab1146 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,11 @@ ## Установка через fyne: 1. go install fyne.io/fyne/v2/cmd/fyne@latest -2. fyne get git.kor-elf.net/kor-elf/gui-for-ffmpeg/src +2. fyne get git.kor-elf.net/kor-elf/gui-for-ffmpeg ## Скомпилировать через исходники: 1. git clone https://git.kor-elf.net/kor-elf/gui-for-ffmpeg.git -2. Переходим в папку проекта и там переходим в папку src: **cd gui-for-ffmpeg/src** +2. Переходим в папку проекта и там переходим в папку src: **cd gui-for-ffmpeg** 3. Ознакамливаемся, что нужно ещё установить для Вашей ОС для простого запуска (через go run) тут: https://docs.fyne.io/started/ 4. *(не обязательный шаг)* Просто запустить можно так: **go run main.go** 5. go install github.com/fyne-io/fyne-cross@latest @@ -35,9 +35,8 @@ ## Работа с переводами: 1. go install -v github.com/nicksnyder/go-i18n/v2/goi18n@latest -2. Переходим в папке проекта в папку src: **cd ./src** -3. goi18n merge -sourceLanguage ru -outdir languages languages/active.\*.toml languages/translate.\*.toml -4. В файлах **languages/translate.\*.toml** переводим текст на нужный язык -5. goi18n merge -sourceLanguage ru -outdir languages languages/active.\*.toml languages/translate.\*.toml +2. goi18n merge -sourceLanguage ru -outdir languages languages/active.\*.toml languages/translate.\*.toml +3. В файлах **languages/translate.\*.toml** переводим текст на нужный язык +4. 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/src/convertor/repository.go b/convertor/repository.go similarity index 94% rename from src/convertor/repository.go rename to convertor/repository.go index 3f26bf5..e31f64a 100644 --- a/src/convertor/repository.go +++ b/convertor/repository.go @@ -1,7 +1,7 @@ package convertor import ( - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/src/setting" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/setting" ) type RepositoryContract interface { diff --git a/src/convertor/service.go b/convertor/service.go similarity index 98% rename from src/convertor/service.go rename to convertor/service.go index bdeb256..9b89f1f 100644 --- a/src/convertor/service.go +++ b/convertor/service.go @@ -2,7 +2,7 @@ package convertor import ( "errors" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/src/helper" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/helper" "io" "os/exec" "regexp" diff --git a/src/convertor/view.go b/convertor/view.go similarity index 98% rename from src/convertor/view.go rename to convertor/view.go index 7a18160..2ac293d 100644 --- a/src/convertor/view.go +++ b/convertor/view.go @@ -8,8 +8,8 @@ import ( "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/storage" "fyne.io/fyne/v2/widget" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/src/helper" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/src/localizer" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/helper" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/localizer" "github.com/nicksnyder/go-i18n/v2/i18n" "image/color" "path/filepath" diff --git a/src/convertor/view_setting.go b/convertor/view_setting.go similarity index 98% rename from src/convertor/view_setting.go rename to convertor/view_setting.go index c09acba..703259f 100644 --- a/src/convertor/view_setting.go +++ b/convertor/view_setting.go @@ -7,7 +7,7 @@ import ( "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/storage" "fyne.io/fyne/v2/widget" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/src/helper" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/helper" "github.com/nicksnyder/go-i18n/v2/i18n" "image/color" "net/url" diff --git a/src/convertor/view_setting_button_download_ffmpeg_anyos.go b/convertor/view_setting_button_download_ffmpeg_anyos.go similarity index 100% rename from src/convertor/view_setting_button_download_ffmpeg_anyos.go rename to convertor/view_setting_button_download_ffmpeg_anyos.go diff --git a/src/convertor/view_setting_button_download_ffmpeg_windows.go b/convertor/view_setting_button_download_ffmpeg_windows.go similarity index 100% rename from src/convertor/view_setting_button_download_ffmpeg_windows.go rename to convertor/view_setting_button_download_ffmpeg_windows.go diff --git a/src/data/.gitignore b/data/.gitignore similarity index 100% rename from src/data/.gitignore rename to data/.gitignore diff --git a/src/error/view.go b/error/view.go similarity index 96% rename from src/error/view.go rename to error/view.go index 61bd0f4..f34d72b 100644 --- a/src/error/view.go +++ b/error/view.go @@ -4,7 +4,7 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/widget" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/src/localizer" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/localizer" "github.com/nicksnyder/go-i18n/v2/i18n" ) diff --git a/src/go.mod b/go.mod similarity index 97% rename from src/go.mod rename to go.mod index 66edcf3..89427e3 100644 --- a/src/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module git.kor-elf.net/kor-elf/gui-for-ffmpeg/src +module git.kor-elf.net/kor-elf/gui-for-ffmpeg go 1.21 diff --git a/src/go.sum b/go.sum similarity index 100% rename from src/go.sum rename to go.sum diff --git a/src/handler/convertor.go b/handler/convertor.go similarity index 97% rename from src/handler/convertor.go rename to handler/convertor.go index e878665..5a2cff1 100644 --- a/src/handler/convertor.go +++ b/handler/convertor.go @@ -4,9 +4,9 @@ import ( "bufio" "errors" "fyne.io/fyne/v2/widget" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/src/convertor" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/src/helper" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/src/localizer" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/convertor" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/helper" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/localizer" "github.com/nicksnyder/go-i18n/v2/i18n" "io" "regexp" diff --git a/src/handler/convertor_anyos.go b/handler/convertor_anyos.go similarity index 87% rename from src/handler/convertor_anyos.go rename to handler/convertor_anyos.go index 16b85a0..4db2fd4 100644 --- a/src/handler/convertor_anyos.go +++ b/handler/convertor_anyos.go @@ -6,7 +6,7 @@ package handler import ( "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/widget" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/src/convertor" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/convertor" ) func getPathsToFF() []convertor.FFPathUtilities { diff --git a/src/handler/convertor_windows.go b/handler/convertor_windows.go similarity index 100% rename from src/handler/convertor_windows.go rename to handler/convertor_windows.go diff --git a/src/handler/main.go b/handler/main.go similarity index 94% rename from src/handler/main.go rename to handler/main.go index 7253b05..48b99a4 100644 --- a/src/handler/main.go +++ b/handler/main.go @@ -1,7 +1,7 @@ package handler import ( - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/src/localizer" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/localizer" ) type MainHandler struct { diff --git a/src/handler/menu.go b/handler/menu.go similarity index 97% rename from src/handler/menu.go rename to handler/menu.go index bf7e332..eabbef2 100644 --- a/src/handler/menu.go +++ b/handler/menu.go @@ -2,8 +2,8 @@ package handler import ( "fyne.io/fyne/v2" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/src/localizer" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/src/menu" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/localizer" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/menu" "github.com/nicksnyder/go-i18n/v2/i18n" ) diff --git a/src/helper/helper.go b/helper/helper.go similarity index 100% rename from src/helper/helper.go rename to helper/helper.go diff --git a/src/helper/path_separator.go b/helper/path_separator.go similarity index 100% rename from src/helper/path_separator.go rename to helper/path_separator.go diff --git a/src/helper/path_separator_window.go b/helper/path_separator_window.go similarity index 100% rename from src/helper/path_separator_window.go rename to helper/path_separator_window.go diff --git a/src/helper/prepare_background_command.go b/helper/prepare_background_command.go similarity index 100% rename from src/helper/prepare_background_command.go rename to helper/prepare_background_command.go diff --git a/src/helper/prepare_background_command_windows.go b/helper/prepare_background_command_windows.go similarity index 100% rename from src/helper/prepare_background_command_windows.go rename to helper/prepare_background_command_windows.go diff --git a/src/icon.png b/icon.png similarity index 100% rename from src/icon.png rename to icon.png diff --git a/src/languages/active.en.toml b/languages/active.en.toml similarity index 100% rename from src/languages/active.en.toml rename to languages/active.en.toml diff --git a/src/languages/active.kk.toml b/languages/active.kk.toml similarity index 100% rename from src/languages/active.kk.toml rename to languages/active.kk.toml diff --git a/src/languages/active.ru.toml b/languages/active.ru.toml similarity index 100% rename from src/languages/active.ru.toml rename to languages/active.ru.toml diff --git a/src/languages/translate.en.toml b/languages/translate.en.toml similarity index 100% rename from src/languages/translate.en.toml rename to languages/translate.en.toml diff --git a/src/languages/translate.kk.toml b/languages/translate.kk.toml similarity index 100% rename from src/languages/translate.kk.toml rename to languages/translate.kk.toml diff --git a/src/localizer/repository.go b/localizer/repository.go similarity index 91% rename from src/localizer/repository.go rename to localizer/repository.go index 03c7fa3..46e36f2 100644 --- a/src/localizer/repository.go +++ b/localizer/repository.go @@ -1,7 +1,7 @@ package localizer import ( - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/src/setting" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/setting" ) type RepositoryContract interface { diff --git a/src/localizer/service.go b/localizer/service.go similarity index 100% rename from src/localizer/service.go rename to localizer/service.go diff --git a/src/localizer/view.go b/localizer/view.go similarity index 100% rename from src/localizer/view.go rename to localizer/view.go diff --git a/src/main.go b/main.go similarity index 89% rename from src/main.go rename to main.go index f992a23..8b1dbc1 100644 --- a/src/main.go +++ b/main.go @@ -6,13 +6,13 @@ import ( "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/widget" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/src/convertor" - error2 "git.kor-elf.net/kor-elf/gui-for-ffmpeg/src/error" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/src/handler" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/src/localizer" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/src/menu" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/src/migration" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/src/setting" + "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" + "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/migration" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/setting" _ "github.com/mattn/go-sqlite3" "golang.org/x/text/language" "gorm.io/driver/sqlite" diff --git a/src/menu/view.go b/menu/view.go similarity index 99% rename from src/menu/view.go rename to menu/view.go index 377dd6e..0dc0896 100644 --- a/src/menu/view.go +++ b/menu/view.go @@ -5,7 +5,7 @@ import ( "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/widget" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/src/localizer" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/localizer" "github.com/nicksnyder/go-i18n/v2/i18n" "golang.org/x/image/colornames" "net/url" diff --git a/src/migration/migration.go b/migration/migration.go similarity index 75% rename from src/migration/migration.go rename to migration/migration.go index 91787ff..932b98a 100644 --- a/src/migration/migration.go +++ b/migration/migration.go @@ -1,7 +1,7 @@ package migration import ( - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/src/setting" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/setting" "gorm.io/gorm" ) diff --git a/src/setting/entity.go b/setting/entity.go similarity index 100% rename from src/setting/entity.go rename to setting/entity.go diff --git a/src/setting/repository.go b/setting/repository.go similarity index 100% rename from src/setting/repository.go rename to setting/repository.go From a1c9143685f357ef3bf3e358cd2ea0f35b043348 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sat, 17 Feb 2024 19:08:58 +0600 Subject: [PATCH 2/7] Added queues. Reworked the architecture. --- convertor/view.go | 136 ++++----- convertor/view_setting.go | 54 ++-- error/view.go | 24 +- handler/convertor.go | 154 ++--------- handler/convertor_anyos.go | 6 +- handler/convertor_windows.go | 6 +- handler/main.go | 9 +- handler/menu.go | 86 +++--- kernel/app.go | 105 +++++++ convertor/service.go => kernel/convertor.go | 38 +-- kernel/error.go | 20 ++ kernel/layout.go | 288 ++++++++++++++++++++ localizer/service.go => kernel/localizer.go | 58 ++-- kernel/queue.go | 137 ++++++++++ kernel/window.go | 67 +++++ languages/active.en.toml | 16 ++ languages/active.kk.toml | 16 ++ languages/active.ru.toml | 4 + languages/translate.en.toml | 18 +- languages/translate.kk.toml | 18 +- localizer/view.go | 24 +- main.go | 104 +++---- menu/view.go | 42 ++- 23 files changed, 996 insertions(+), 434 deletions(-) create mode 100644 kernel/app.go rename convertor/service.go => kernel/convertor.go (80%) create mode 100644 kernel/error.go create mode 100644 kernel/layout.go rename localizer/service.go => kernel/localizer.go (62%) create mode 100644 kernel/queue.go create mode 100644 kernel/window.go diff --git a/convertor/view.go b/convertor/view.go index 2ac293d..0510768 100644 --- a/convertor/view.go +++ b/convertor/view.go @@ -5,11 +5,9 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/storage" "fyne.io/fyne/v2/widget" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/helper" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/localizer" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" "github.com/nicksnyder/go-i18n/v2/i18n" "image/color" "path/filepath" @@ -17,7 +15,7 @@ import ( type ViewContract interface { Main( - runConvert func(setting HandleConvertSetting, progressbar *widget.ProgressBar) error, + runConvert func(setting HandleConvertSetting), ) SelectFFPath( ffmpegPath string, @@ -29,12 +27,11 @@ type ViewContract interface { } type View struct { - w fyne.Window - localizerService localizer.ServiceContract + app kernel.AppContract } type HandleConvertSetting struct { - VideoFileInput *File + VideoFileInput kernel.File DirectoryForSave string OverwriteOutputFiles bool } @@ -45,15 +42,14 @@ type enableFormConversionStruct struct { form *widget.Form } -func NewView(w fyne.Window, localizerService localizer.ServiceContract) *View { +func NewView(app kernel.AppContract) *View { return &View{ - w: w, - localizerService: localizerService, + app: app, } } func (v View) Main( - runConvert func(setting HandleConvertSetting, progressbar *widget.ProgressBar) error, + runConvert func(setting HandleConvertSetting), ) { form := &widget.Form{} @@ -61,13 +57,11 @@ func (v View) Main( conversionMessage.TextSize = 16 conversionMessage.TextStyle = fyne.TextStyle{Bold: true} - progress := widget.NewProgressBar() - - fileVideoForConversion, fileVideoForConversionMessage, fileInput := v.getButtonFileVideoForConversion(form, progress, conversionMessage) + fileVideoForConversion, fileVideoForConversionMessage, fileInput := v.getButtonFileVideoForConversion(form, conversionMessage) buttonForSelectedDir, buttonForSelectedDirMessage, pathToSaveDirectory := v.getButtonForSelectingDirectoryForSaving() isOverwriteOutputFiles := false - checkboxOverwriteOutputFilesTitle := v.localizerService.GetMessage(&i18n.LocalizeConfig{ + checkboxOverwriteOutputFilesTitle := v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "checkboxOverwriteOutputFilesTitle", }) checkboxOverwriteOutputFiles := widget.NewCheck(checkboxOverwriteOutputFilesTitle, func(b bool) { @@ -76,14 +70,14 @@ func (v View) Main( form.Items = []*widget.FormItem{ { - Text: v.localizerService.GetMessage(&i18n.LocalizeConfig{MessageID: "fileVideoForConversionTitle"}), + Text: v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{MessageID: "fileVideoForConversionTitle"}), Widget: fileVideoForConversion, }, { Widget: container.NewHScroll(fileVideoForConversionMessage), }, { - Text: v.localizerService.GetMessage(&i18n.LocalizeConfig{MessageID: "buttonForSelectedDirTitle"}), + Text: v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{MessageID: "buttonForSelectedDirTitle"}), Widget: buttonForSelectedDir, }, { @@ -93,7 +87,7 @@ func (v View) Main( Widget: checkboxOverwriteOutputFiles, }, } - form.SubmitText = v.localizerService.GetMessage(&i18n.LocalizeConfig{ + form.SubmitText = v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "converterVideoFilesSubmitTitle", }) @@ -105,7 +99,7 @@ func (v View) Main( form.OnSubmit = func() { if len(*pathToSaveDirectory) == 0 { - showConversionMessage(conversionMessage, errors.New(v.localizerService.GetMessage(&i18n.LocalizeConfig{ + showConversionMessage(conversionMessage, errors.New(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "errorSelectedFolderSave", }))) enableFormConversion(enableFormConversionStruct) @@ -118,71 +112,61 @@ func (v View) Main( form.Disable() setting := HandleConvertSetting{ - VideoFileInput: fileInput, + VideoFileInput: *fileInput, DirectoryForSave: *pathToSaveDirectory, OverwriteOutputFiles: isOverwriteOutputFiles, } - err := runConvert(setting, progress) - if err != nil { - showConversionMessage(conversionMessage, err) - enableFormConversion(enableFormConversionStruct) - return - } + runConvert(setting) enableFormConversion(enableFormConversionStruct) + + fileVideoForConversionMessage.Text = "" + form.Disable() } - converterVideoFilesTitle := v.localizerService.GetMessage(&i18n.LocalizeConfig{ + converterVideoFilesTitle := v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "converterVideoFilesTitle", }) - v.w.SetContent(widget.NewCard(converterVideoFilesTitle, "", container.NewVBox(form, conversionMessage, progress))) + v.app.GetWindow().SetContent(widget.NewCard(converterVideoFilesTitle, "", container.NewVBox(form, conversionMessage))) form.Disable() } -func (v View) getButtonFileVideoForConversion(form *widget.Form, progress *widget.ProgressBar, conversionMessage *canvas.Text) (*widget.Button, *canvas.Text, *File) { - fileInput := &File{} +func (v View) getButtonFileVideoForConversion(form *widget.Form, conversionMessage *canvas.Text) (*widget.Button, *canvas.Text, *kernel.File) { + fileInput := &kernel.File{} fileVideoForConversionMessage := canvas.NewText("", color.RGBA{R: 255, G: 0, B: 0, A: 255}) fileVideoForConversionMessage.TextSize = 16 fileVideoForConversionMessage.TextStyle = fyne.TextStyle{Bold: true} - buttonTitle := v.localizerService.GetMessage(&i18n.LocalizeConfig{ + buttonTitle := v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "choose", }) var locationURI fyne.ListableURI button := widget.NewButton(buttonTitle, func() { - fileDialog := dialog.NewFileOpen( - func(r fyne.URIReadCloser, err error) { - if err != nil { - fileVideoForConversionMessage.Text = err.Error() - setStringErrorStyle(fileVideoForConversionMessage) - return - } - if r == nil { - return - } + v.app.GetWindow().NewFileOpen(func(r fyne.URIReadCloser, err error) { + if err != nil { + fileVideoForConversionMessage.Text = err.Error() + setStringErrorStyle(fileVideoForConversionMessage) + return + } + if r == nil { + return + } - fileInput.Path = r.URI().Path() - fileInput.Name = r.URI().Name() - fileInput.Ext = r.URI().Extension() + fileInput.Path = r.URI().Path() + fileInput.Name = r.URI().Name() + fileInput.Ext = r.URI().Extension() - fileVideoForConversionMessage.Text = r.URI().Path() - setStringSuccessStyle(fileVideoForConversionMessage) + fileVideoForConversionMessage.Text = r.URI().Path() + setStringSuccessStyle(fileVideoForConversionMessage) - form.Enable() - progress.Value = 0 - progress.Refresh() - conversionMessage.Text = "" + form.Enable() + conversionMessage.Text = "" - listableURI := storage.NewFileURI(filepath.Dir(r.URI().Path())) - locationURI, err = storage.ListerForURI(listableURI) - }, v.w) - helper.FileDialogResize(fileDialog, v.w) - fileDialog.Show() - if locationURI != nil { - fileDialog.SetLocation(locationURI) - } + listableURI := storage.NewFileURI(filepath.Dir(r.URI().Path())) + locationURI, err = storage.ListerForURI(listableURI) + }, locationURI) }) return button, fileVideoForConversionMessage, fileInput @@ -196,36 +180,30 @@ func (v View) getButtonForSelectingDirectoryForSaving() (button *widget.Button, path := "" dirPath = &path - buttonTitle := v.localizerService.GetMessage(&i18n.LocalizeConfig{ + buttonTitle := v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "choose", }) var locationURI fyne.ListableURI button = widget.NewButton(buttonTitle, func() { - fileDialog := dialog.NewFolderOpen( - func(r fyne.ListableURI, err error) { - if err != nil { - buttonMessage.Text = err.Error() - setStringErrorStyle(buttonMessage) - return - } - if r == nil { - return - } + v.app.GetWindow().NewFolderOpen(func(r fyne.ListableURI, err error) { + if err != nil { + buttonMessage.Text = err.Error() + setStringErrorStyle(buttonMessage) + return + } + if r == nil { + return + } - path = r.Path() + path = r.Path() - buttonMessage.Text = r.Path() - setStringSuccessStyle(buttonMessage) - locationURI, _ = storage.ListerForURI(r) + buttonMessage.Text = r.Path() + setStringSuccessStyle(buttonMessage) + locationURI, _ = storage.ListerForURI(r) - }, v.w) - helper.FileDialogResize(fileDialog, v.w) - fileDialog.Show() - if locationURI != nil { - fileDialog.SetLocation(locationURI) - } + }, locationURI) }) return diff --git a/convertor/view_setting.go b/convertor/view_setting.go index 703259f..62e8ad5 100644 --- a/convertor/view_setting.go +++ b/convertor/view_setting.go @@ -4,10 +4,8 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/storage" "fyne.io/fyne/v2/widget" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/helper" "github.com/nicksnyder/go-i18n/v2/i18n" "image/color" "net/url" @@ -37,13 +35,13 @@ func (v View) SelectFFPath( form := &widget.Form{ Items: []*widget.FormItem{ { - Text: v.localizerService.GetMessage(&i18n.LocalizeConfig{ + Text: v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "titleDownloadLink", }), Widget: link, }, { - Text: v.localizerService.GetMessage(&i18n.LocalizeConfig{ + Text: v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "pathToFfmpeg", }), Widget: buttonFFmpeg, @@ -52,7 +50,7 @@ func (v View) SelectFFPath( Widget: container.NewHScroll(buttonFFmpegMessage), }, { - Text: v.localizerService.GetMessage(&i18n.LocalizeConfig{ + Text: v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "pathToFfprobe", }), Widget: buttonFFprobe, @@ -64,7 +62,7 @@ func (v View) SelectFFPath( Widget: errorMessage, }, }, - SubmitText: v.localizerService.GetMessage(&i18n.LocalizeConfig{ + SubmitText: v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "save", }), OnSubmit: func() { @@ -76,15 +74,15 @@ func (v View) SelectFFPath( } if cancel != nil { form.OnCancel = cancel - form.CancelText = v.localizerService.GetMessage(&i18n.LocalizeConfig{ + form.CancelText = v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "cancel", }) } - selectFFPathTitle := v.localizerService.GetMessage(&i18n.LocalizeConfig{ + selectFFPathTitle := v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "selectFFPathTitle", }) - v.w.SetContent(widget.NewCard(selectFFPathTitle, "", container.NewVBox( + v.app.GetWindow().SetContent(widget.NewCard(selectFFPathTitle, "", container.NewVBox( form, v.blockDownloadFFmpeg(donwloadFFmpeg), ))) @@ -97,7 +95,7 @@ func (v View) getButtonSelectFile(path string) (filePath *string, button *widget buttonMessage.TextSize = 16 buttonMessage.TextStyle = fyne.TextStyle{Bold: true} - buttonTitle := v.localizerService.GetMessage(&i18n.LocalizeConfig{ + buttonTitle := v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "choose", }) @@ -108,30 +106,24 @@ func (v View) getButtonSelectFile(path string) (filePath *string, button *widget } button = widget.NewButton(buttonTitle, func() { - fileDialog := dialog.NewFileOpen( - func(r fyne.URIReadCloser, err error) { - if err != nil { - buttonMessage.Text = err.Error() - setStringErrorStyle(buttonMessage) - return - } - if r == nil { - return - } + 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() + path = r.URI().Path() - buttonMessage.Text = r.URI().Path() - setStringSuccessStyle(buttonMessage) + buttonMessage.Text = r.URI().Path() + setStringSuccessStyle(buttonMessage) - listableURI := storage.NewFileURI(filepath.Dir(r.URI().Path())) - locationURI, _ = storage.ListerForURI(listableURI) - }, v.w) - helper.FileDialogResize(fileDialog, v.w) - fileDialog.Show() - if locationURI != nil { - fileDialog.SetLocation(locationURI) - } + listableURI := storage.NewFileURI(filepath.Dir(r.URI().Path())) + locationURI, _ = storage.ListerForURI(listableURI) + }, locationURI) }) return diff --git a/error/view.go b/error/view.go index f34d72b..4845ab0 100644 --- a/error/view.go +++ b/error/view.go @@ -1,9 +1,9 @@ package error import ( - "fyne.io/fyne/v2" "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" "github.com/nicksnyder/go-i18n/v2/i18n" ) @@ -13,23 +13,21 @@ type ViewContract interface { } type View struct { - w fyne.Window - localizerService localizer.ServiceContract + app kernel.AppContract } -func NewView(w fyne.Window, localizerService localizer.ServiceContract) *View { +func NewView(app kernel.AppContract) *View { return &View{ - w: w, - localizerService: localizerService, + app: app, } } func (v View) PanicError(err error) { - messageHead := v.localizerService.GetMessage(&i18n.LocalizeConfig{ + messageHead := v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "error", }) - v.w.SetContent(container.NewBorder( + v.app.GetWindow().SetContent(container.NewBorder( container.NewVBox( widget.NewLabel(messageHead), widget.NewLabel(err.Error()), @@ -37,21 +35,21 @@ func (v View) PanicError(err error) { nil, nil, nil, - localizer.LanguageSelectionForm(v.localizerService, func(lang localizer.Lang) { + localizer.LanguageSelectionForm(v.app.GetLocalizerService(), func(lang kernel.Lang) { v.PanicError(err) }), )) } func (v View) PanicErrorWriteDirectoryData() { - message := v.localizerService.GetMessage(&i18n.LocalizeConfig{ + message := v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "errorDatabase", }) - messageHead := v.localizerService.GetMessage(&i18n.LocalizeConfig{ + messageHead := v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "error", }) - v.w.SetContent(container.NewBorder( + v.app.GetWindow().SetContent(container.NewBorder( container.NewVBox( widget.NewLabel(messageHead), widget.NewLabel(message), @@ -59,7 +57,7 @@ func (v View) PanicErrorWriteDirectoryData() { nil, nil, nil, - localizer.LanguageSelectionForm(v.localizerService, func(lang localizer.Lang) { + localizer.LanguageSelectionForm(v.app.GetLocalizerService(), func(lang kernel.Lang) { v.PanicErrorWriteDirectoryData() }), )) diff --git a/handler/convertor.go b/handler/convertor.go index 5a2cff1..909e908 100644 --- a/handler/convertor.go +++ b/handler/convertor.go @@ -1,17 +1,11 @@ package handler import ( - "bufio" "errors" - "fyne.io/fyne/v2/widget" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/convertor" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/helper" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/localizer" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" "github.com/nicksnyder/go-i18n/v2/i18n" - "io" - "regexp" - "strconv" - "strings" ) type ConvertorHandlerContract interface { @@ -22,23 +16,20 @@ type ConvertorHandlerContract interface { } type ConvertorHandler struct { - convertorService convertor.ServiceContract + app kernel.AppContract convertorView convertor.ViewContract convertorRepository convertor.RepositoryContract - localizerService localizer.ServiceContract } func NewConvertorHandler( - convertorService convertor.ServiceContract, + app kernel.AppContract, convertorView convertor.ViewContract, convertorRepository convertor.RepositoryContract, - localizerService localizer.ServiceContract, ) *ConvertorHandler { return &ConvertorHandler{ - convertorService: convertorService, + app: app, convertorView: convertorView, convertorRepository: convertorRepository, - localizerService: localizerService, } } @@ -57,32 +48,23 @@ func (h ConvertorHandler) FfPathSelection() { } func (h ConvertorHandler) GetFfmpegVersion() (string, error) { - return h.convertorService.GetFFmpegVesrion() + return h.app.GetConvertorService().GetFFmpegVesrion() } func (h ConvertorHandler) GetFfprobeVersion() (string, error) { - return h.convertorService.GetFFprobeVersion() + return h.app.GetConvertorService().GetFFprobeVersion() } -func (h ConvertorHandler) runConvert(setting convertor.HandleConvertSetting, progressbar *widget.ProgressBar) error { - totalDuration, err := h.convertorService.GetTotalDuration(setting.VideoFileInput) - if err != nil { - return err - } - progress := NewProgress(totalDuration, progressbar, h.localizerService) - - return h.convertorService.RunConvert( - convertor.ConvertSetting{ - VideoFileInput: setting.VideoFileInput, - VideoFileOut: &convertor.File{ - Path: setting.DirectoryForSave + helper.PathSeparator() + setting.VideoFileInput.Name + ".mp4", - Name: setting.VideoFileInput.Name, - Ext: ".mp4", - }, - OverwriteOutputFiles: setting.OverwriteOutputFiles, +func (h ConvertorHandler) runConvert(setting convertor.HandleConvertSetting) { + h.app.GetQueue().Add(&kernel.ConvertSetting{ + VideoFileInput: setting.VideoFileInput, + VideoFileOut: kernel.File{ + Path: setting.DirectoryForSave + helper.PathSeparator() + setting.VideoFileInput.Name + ".mp4", + Name: setting.VideoFileInput.Name, + Ext: ".mp4", }, - progress, - ) + OverwriteOutputFiles: setting.OverwriteOutputFiles, + }) } func (h ConvertorHandler) checkingFFPathUtilities() bool { @@ -92,11 +74,11 @@ func (h ConvertorHandler) checkingFFPathUtilities() bool { pathsToFF := getPathsToFF() for _, item := range pathsToFF { - ffmpegChecking, _ := h.convertorService.ChangeFFmpegPath(item.FFmpeg) + ffmpegChecking, _ := h.app.GetConvertorService().ChangeFFmpegPath(item.FFmpeg) if ffmpegChecking == false { continue } - ffprobeChecking, _ := h.convertorService.ChangeFFprobePath(item.FFprobe) + ffprobeChecking, _ := h.app.GetConvertorService().ChangeFFprobePath(item.FFprobe) if ffprobeChecking == false { continue } @@ -109,17 +91,17 @@ func (h ConvertorHandler) checkingFFPathUtilities() bool { } func (h ConvertorHandler) saveSettingFFPath(ffmpegPath string, ffprobePath string) error { - ffmpegChecking, _ := h.convertorService.ChangeFFmpegPath(ffmpegPath) + ffmpegChecking, _ := h.app.GetConvertorService().ChangeFFmpegPath(ffmpegPath) if ffmpegChecking == false { - errorText := h.localizerService.GetMessage(&i18n.LocalizeConfig{ + errorText := h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "errorFFmpeg", }) return errors.New(errorText) } - ffprobeChecking, _ := h.convertorService.ChangeFFprobePath(ffprobePath) + ffprobeChecking, _ := h.app.GetConvertorService().ChangeFFprobePath(ffprobePath) if ffprobeChecking == false { - errorText := h.localizerService.GetMessage(&i18n.LocalizeConfig{ + errorText := h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "errorFFprobe", }) return errors.New(errorText) @@ -134,105 +116,15 @@ func (h ConvertorHandler) saveSettingFFPath(ffmpegPath string, ffprobePath strin } func (h ConvertorHandler) checkingFFPath() bool { - _, err := h.convertorService.GetFFmpegVesrion() + _, err := h.app.GetConvertorService().GetFFmpegVesrion() if err != nil { return false } - _, err = h.convertorService.GetFFprobeVersion() + _, err = h.app.GetConvertorService().GetFFprobeVersion() if err != nil { return false } return true } - -type Progress struct { - totalDuration float64 - progressbar *widget.ProgressBar - protocol string - localizerService localizer.ServiceContract -} - -func NewProgress(totalDuration float64, progressbar *widget.ProgressBar, localizerService localizer.ServiceContract) 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 - 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 - 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 - p.progressbar.Refresh() - } - } - - if isProcessCompleted == false { - if len(errorText) == 0 { - errorText = p.localizerService.GetMessage(&i18n.LocalizeConfig{ - MessageID: "errorConverter", - }) - } - return errors.New(errorText) - } - - return nil -} diff --git a/handler/convertor_anyos.go b/handler/convertor_anyos.go index 4db2fd4..409c524 100644 --- a/handler/convertor_anyos.go +++ b/handler/convertor_anyos.go @@ -6,11 +6,11 @@ package handler import ( "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/widget" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/convertor" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" ) -func getPathsToFF() []convertor.FFPathUtilities { - return []convertor.FFPathUtilities{{"ffmpeg/bin/ffmpeg", "ffmpeg/bin/ffprobe"}, {"ffmpeg", "ffprobe"}} +func getPathsToFF() []kernel.FFPathUtilities { + return []kernel.FFPathUtilities{{"ffmpeg/bin/ffmpeg", "ffmpeg/bin/ffprobe"}, {"ffmpeg", "ffprobe"}} } func (h ConvertorHandler) downloadFFmpeg(progressBar *widget.ProgressBar, progressMessage *canvas.Text) (err error) { diff --git a/handler/convertor_windows.go b/handler/convertor_windows.go index 79732c5..14984fc 100644 --- a/handler/convertor_windows.go +++ b/handler/convertor_windows.go @@ -8,7 +8,7 @@ import ( "errors" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/widget" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/src/convertor" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" "github.com/nicksnyder/go-i18n/v2/i18n" "io" "net/http" @@ -17,8 +17,8 @@ import ( "strings" ) -func getPathsToFF() []convertor.FFPathUtilities { - return []convertor.FFPathUtilities{{"ffmpeg\\bin\\ffmpeg.exe", "ffmpeg\\bin\\ffprobe.exe"}} +func getPathsToFF() []kernel.FFPathUtilities { + return []kernel.FFPathUtilities{{"ffmpeg\\bin\\ffmpeg.exe", "ffmpeg\\bin\\ffprobe.exe"}} } func (h ConvertorHandler) downloadFFmpeg(progressBar *widget.ProgressBar, progressMessage *canvas.Text) (err error) { diff --git a/handler/main.go b/handler/main.go index 48b99a4..e2e6398 100644 --- a/handler/main.go +++ b/handler/main.go @@ -1,27 +1,28 @@ package handler import ( + "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 - localizerService localizer.ServiceContract } func NewMainHandler( + app kernel.AppContract, convertorHandler ConvertorHandlerContract, menuHandler MenuHandlerContract, localizerRepository localizer.RepositoryContract, - localizerService localizer.ServiceContract, ) *MainHandler { return &MainHandler{ + app: app, convertorHandler: convertorHandler, menuHandler: menuHandler, localizerRepository: localizerRepository, - localizerService: localizerService, } } @@ -31,7 +32,7 @@ func (h MainHandler) Start() { h.menuHandler.LanguageSelection() return } - _ = h.localizerService.SetCurrentLanguageByCode(language) + _ = h.app.GetLocalizerService().SetCurrentLanguageByCode(language) h.convertorHandler.MainConvertor() } diff --git a/handler/menu.go b/handler/menu.go index eabbef2..ca38d52 100644 --- a/handler/menu.go +++ b/handler/menu.go @@ -2,6 +2,7 @@ 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" "github.com/nicksnyder/go-i18n/v2/i18n" @@ -12,34 +13,30 @@ type MenuHandlerContract interface { LanguageSelection() } -type menuItems struct { - menuItem map[string]*fyne.MenuItem - menu map[string]*fyne.Menu -} - type MenuHandler struct { + app kernel.AppContract convertorHandler ConvertorHandlerContract menuView menu.ViewContract - localizerService localizer.ServiceContract localizerView localizer.ViewContract localizerRepository localizer.RepositoryContract - menuItems *menuItems + localizerListener localizerListenerContract } func NewMenuHandler( + app kernel.AppContract, convertorHandler ConvertorHandlerContract, menuView menu.ViewContract, - localizerService localizer.ServiceContract, localizerView localizer.ViewContract, localizerRepository localizer.RepositoryContract, + localizerListener localizerListenerContract, ) *MenuHandler { return &MenuHandler{ + app: app, convertorHandler: convertorHandler, menuView: menuView, - localizerService: localizerService, localizerView: localizerView, localizerRepository: localizerRepository, - menuItems: &menuItems{menuItem: map[string]*fyne.MenuItem{}, menu: map[string]*fyne.Menu{}}, + localizerListener: localizerListener, } } @@ -51,40 +48,40 @@ func (h MenuHandler) GetMainMenu() *fyne.MainMenu { } func (h MenuHandler) getMenuSettings() *fyne.Menu { - quit := fyne.NewMenuItem(h.localizerService.GetMessage(&i18n.LocalizeConfig{ + quit := fyne.NewMenuItem(h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "exit", }), nil) quit.IsQuit = true - h.menuItems.menuItem["exit"] = quit + h.localizerListener.AddMenuItem("exit", quit) - languageSelection := fyne.NewMenuItem(h.localizerService.GetMessage(&i18n.LocalizeConfig{ + languageSelection := fyne.NewMenuItem(h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "changeLanguage", }), h.LanguageSelection) - h.menuItems.menuItem["changeLanguage"] = languageSelection + h.localizerListener.AddMenuItem("changeLanguage", languageSelection) - ffPathSelection := fyne.NewMenuItem(h.localizerService.GetMessage(&i18n.LocalizeConfig{ + ffPathSelection := fyne.NewMenuItem(h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "changeFFPath", }), h.convertorHandler.FfPathSelection) - h.menuItems.menuItem["changeFFPath"] = ffPathSelection + h.localizerListener.AddMenuItem("changeFFPath", ffPathSelection) - settings := fyne.NewMenu(h.localizerService.GetMessage(&i18n.LocalizeConfig{ + settings := fyne.NewMenu(h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "settings", }), languageSelection, ffPathSelection, quit) - h.menuItems.menu["settings"] = settings + h.localizerListener.AddMenu("settings", settings) return settings } func (h MenuHandler) getMenuHelp() *fyne.Menu { - about := fyne.NewMenuItem(h.localizerService.GetMessage(&i18n.LocalizeConfig{ + about := fyne.NewMenuItem(h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "about", }), h.openAbout) - h.menuItems.menuItem["about"] = about + h.localizerListener.AddMenuItem("about", about) - help := fyne.NewMenu(h.localizerService.GetMessage(&i18n.LocalizeConfig{ + help := fyne.NewMenu(h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "help", }), about) - h.menuItems.menu["help"] = help + h.localizerListener.AddMenu("help", help) return help } @@ -92,13 +89,13 @@ func (h MenuHandler) getMenuHelp() *fyne.Menu { func (h MenuHandler) openAbout() { ffmpeg, err := h.convertorHandler.GetFfmpegVersion() if err != nil { - ffmpeg = h.localizerService.GetMessage(&i18n.LocalizeConfig{ + ffmpeg = h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "errorFFmpegVersion", }) } ffprobe, err := h.convertorHandler.GetFfprobeVersion() if err != nil { - ffprobe = h.localizerService.GetMessage(&i18n.LocalizeConfig{ + ffprobe = h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "errorFFprobeVersion", }) } @@ -107,19 +104,46 @@ func (h MenuHandler) openAbout() { } func (h MenuHandler) LanguageSelection() { - h.localizerView.LanguageSelection(func(lang localizer.Lang) { + h.localizerView.LanguageSelection(func(lang kernel.Lang) { _, _ = h.localizerRepository.Save(lang.Code) - h.menuMessageReload() h.convertorHandler.MainConvertor() }) } -func (h MenuHandler) menuMessageReload() { - for messageID, menu := range h.menuItems.menuItem { - menu.Label = h.localizerService.GetMessage(&i18n.LocalizeConfig{MessageID: messageID}) +type menuItems struct { + menuItem map[string]*fyne.MenuItem + menu map[string]*fyne.Menu +} + +type LocalizerListener struct { + menuItems *menuItems +} + +type localizerListenerContract interface { + AddMenu(messageID string, menu *fyne.Menu) + AddMenuItem(messageID string, menuItem *fyne.MenuItem) +} + +func NewLocalizerListener() *LocalizerListener { + return &LocalizerListener{ + &menuItems{menuItem: map[string]*fyne.MenuItem{}, menu: map[string]*fyne.Menu{}}, } - for messageID, menu := range h.menuItems.menu { - menu.Label = h.localizerService.GetMessage(&i18n.LocalizeConfig{MessageID: messageID}) +} + +func (l LocalizerListener) AddMenu(messageID string, menu *fyne.Menu) { + l.menuItems.menu[messageID] = menu +} + +func (l LocalizerListener) AddMenuItem(messageID string, menuItem *fyne.MenuItem) { + l.menuItems.menuItem[messageID] = menuItem +} + +func (l LocalizerListener) Change(localizerService kernel.LocalizerContract) { + for messageID, menu := range l.menuItems.menuItem { + menu.Label = localizerService.GetMessage(&i18n.LocalizeConfig{MessageID: messageID}) + } + for messageID, menu := range l.menuItems.menu { + menu.Label = localizerService.GetMessage(&i18n.LocalizeConfig{MessageID: messageID}) menu.Refresh() } } diff --git a/kernel/app.go b/kernel/app.go new file mode 100644 index 0000000..54a82f1 --- /dev/null +++ b/kernel/app.go @@ -0,0 +1,105 @@ +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 + AfterClosing() + RunConvertor() +} + +type App struct { + AppFyne fyne.App + Window WindowContract + Queue QueueListContract + + localizerService LocalizerContract + convertorService ConvertorContract +} + +func NewApp( + metadata *fyne.AppMetadata, + localizerService LocalizerContract, + queue QueueListContract, + queueLayoutObject QueueLayoutObjectContract, + convertorService ConvertorContract, +) *App { + app.SetMetadata(*metadata) + a := app.New() + + return &App{ + AppFyne: a, + Window: newWindow(a.NewWindow("GUI for FFmpeg"), NewLayout(queueLayoutObject, localizerService)), + Queue: queue, + + localizerService: localizerService, + convertorService: convertorService, + } +} + +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) 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) + + totalDuration, err := a.convertorService.GetTotalDuration(&queue.Setting.VideoFileInput) + if err != nil { + queue.Status = StatusType(Error) + queue.Error = err + a.Window.GetLayout().ChangeQueueStatus(queueId, queue) + continue + } + progress := a.Window.GetLayout().NewProgressbar(queueId, totalDuration) + + err = a.convertorService.RunConvert(*queue.Setting, progress) + if err != nil { + queue.Status = StatusType(Error) + queue.Error = err + a.Window.GetLayout().ChangeQueueStatus(queueId, queue) + continue + } + queue.Status = StatusType(Completed) + a.Window.GetLayout().ChangeQueueStatus(queueId, queue) + } + }() +} diff --git a/convertor/service.go b/kernel/convertor.go similarity index 80% rename from convertor/service.go rename to kernel/convertor.go index 9b89f1f..8441fa1 100644 --- a/convertor/service.go +++ b/kernel/convertor.go @@ -1,4 +1,4 @@ -package convertor +package kernel import ( "errors" @@ -10,7 +10,7 @@ import ( "strings" ) -type ServiceContract interface { +type ConvertorContract interface { RunConvert(setting ConvertSetting, progress ProgressContract) error GetTotalDuration(file *File) (float64, error) GetFFmpegVesrion() (string, error) @@ -35,35 +35,23 @@ type runningProcesses struct { numberOfStarts int } -type Service struct { +type Convertor struct { ffPathUtilities *FFPathUtilities runningProcesses runningProcesses } -type File struct { - Path string - Name string - Ext string -} - -type ConvertSetting struct { - VideoFileInput *File - VideoFileOut *File - OverwriteOutputFiles bool -} - type ConvertData struct { totalDuration float64 } -func NewService(ffPathUtilities FFPathUtilities) *Service { - return &Service{ - ffPathUtilities: &ffPathUtilities, +func NewService(ffPathUtilities *FFPathUtilities) *Convertor { + return &Convertor{ + ffPathUtilities: ffPathUtilities, runningProcesses: runningProcesses{items: map[int]*exec.Cmd{}, numberOfStarts: 0}, } } -func (s Service) RunConvert(setting ConvertSetting, progress ProgressContract) error { +func (s Convertor) RunConvert(setting ConvertSetting, progress ProgressContract) error { overwriteOutputFiles := "-n" if setting.OverwriteOutputFiles == true { overwriteOutputFiles = "-y" @@ -103,7 +91,7 @@ func (s Service) RunConvert(setting ConvertSetting, progress ProgressContract) e return nil } -func (s Service) GetTotalDuration(file *File) (duration float64, err error) { +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) @@ -118,7 +106,7 @@ func (s Service) GetTotalDuration(file *File) (duration float64, err error) { return strconv.ParseFloat(strings.TrimSpace(string(out)), 64) } -func (s Service) GetFFmpegVesrion() (string, error) { +func (s Convertor) GetFFmpegVesrion() (string, error) { cmd := exec.Command(s.ffPathUtilities.FFmpeg, "-version") helper.PrepareBackgroundCommand(cmd) out, err := cmd.CombinedOutput() @@ -129,7 +117,7 @@ func (s Service) GetFFmpegVesrion() (string, error) { return text[0], nil } -func (s Service) GetFFprobeVersion() (string, error) { +func (s Convertor) GetFFprobeVersion() (string, error) { cmd := exec.Command(s.ffPathUtilities.FFprobe, "-version") helper.PrepareBackgroundCommand(cmd) out, err := cmd.CombinedOutput() @@ -140,7 +128,7 @@ func (s Service) GetFFprobeVersion() (string, error) { return text[0], nil } -func (s Service) ChangeFFmpegPath(path string) (bool, error) { +func (s Convertor) ChangeFFmpegPath(path string) (bool, error) { cmd := exec.Command(path, "-version") helper.PrepareBackgroundCommand(cmd) out, err := cmd.CombinedOutput() @@ -154,7 +142,7 @@ func (s Service) ChangeFFmpegPath(path string) (bool, error) { return true, nil } -func (s Service) ChangeFFprobePath(path string) (bool, error) { +func (s Convertor) ChangeFFprobePath(path string) (bool, error) { cmd := exec.Command(path, "-version") helper.PrepareBackgroundCommand(cmd) out, err := cmd.CombinedOutput() @@ -168,6 +156,6 @@ func (s Service) ChangeFFprobePath(path string) (bool, error) { return true, nil } -func (s Service) GetRunningProcesses() map[int]*exec.Cmd { +func (s Convertor) GetRunningProcesses() map[int]*exec.Cmd { return s.runningProcesses.items } diff --git a/kernel/error.go b/kernel/error.go new file mode 100644 index 0000000..7f03ea1 --- /dev/null +++ b/kernel/error.go @@ -0,0 +1,20 @@ +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() + 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/layout.go b/kernel/layout.go new file mode 100644 index 0000000..4b740d4 --- /dev/null +++ b/kernel/layout.go @@ -0,0 +1,288 @@ +package kernel + +import ( + "bufio" + "errors" + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" + "github.com/nicksnyder/go-i18n/v2/i18n" + "image/color" + "io" + "regexp" + "strconv" + "strings" +) + +type LayoutContract interface { + SetContent(content fyne.CanvasObject) *fyne.Container + NewProgressbar(queueId int, totalDuration float64) ProgressContract + ChangeQueueStatus(queueId int, queue *Queue) +} + +type Layout struct { + layout *fyne.Container + queueLayoutObject QueueLayoutObjectContract + localizerService LocalizerContract +} + +func NewLayout(queueLayoutObject QueueLayoutObjectContract, localizerService LocalizerContract) *Layout { + layout := container.NewAdaptiveGrid(2, widget.NewLabel(""), container.NewVScroll(queueLayoutObject.GetCanvasObject())) + + return &Layout{ + layout: layout, + queueLayoutObject: queueLayoutObject, + localizerService: localizerService, + } +} + +func (l Layout) SetContent(content fyne.CanvasObject) *fyne.Container { + l.layout.Objects[0] = content + return l.layout +} + +func (l Layout) NewProgressbar(queueId int, totalDuration float64) ProgressContract { + progressbar := l.queueLayoutObject.GetProgressbar(queueId) + return NewProgress(totalDuration, progressbar, l.localizerService) +} + +func (l Layout) ChangeQueueStatus(queueId int, queue *Queue) { + l.queueLayoutObject.ChangeQueueStatus(queueId, queue) +} + +type QueueLayoutObjectContract interface { + GetCanvasObject() fyne.CanvasObject + GetProgressbar(queueId int) *widget.ProgressBar + ChangeQueueStatus(queueId int, queue *Queue) +} + +type QueueLayoutObject struct { + QueueListContract QueueListContract + + queue QueueListContract + container *fyne.Container + items map[int]QueueLayoutItem + localizerService LocalizerContract + layoutLocalizerListener LayoutLocalizerListenerContract +} + +type QueueLayoutItem struct { + CanvasObject fyne.CanvasObject + ProgressBar *widget.ProgressBar + StatusMessage *canvas.Text + MessageError *canvas.Text +} + +func NewQueueLayoutObject(queue QueueListContract, localizerService LocalizerContract, layoutLocalizerListener LayoutLocalizerListenerContract) *QueueLayoutObject { + title := widget.NewLabel(localizerService.GetMessage(&i18n.LocalizeConfig{MessageID: "queue"}) + ":") + title.TextStyle.Bold = true + + layoutLocalizerListener.AddItem("queue", title) + + queueLayoutObject := &QueueLayoutObject{ + queue: queue, + container: container.NewVBox(title), + items: map[int]QueueLayoutItem{}, + localizerService: localizerService, + layoutLocalizerListener: layoutLocalizerListener, + } + + queue.AddListener(queueLayoutObject) + + return queueLayoutObject +} + +func (o QueueLayoutObject) GetCanvasObject() fyne.CanvasObject { + return o.container +} + +func (o QueueLayoutObject) GetProgressbar(queueId int) *widget.ProgressBar { + if item, ok := o.items[queueId]; ok { + return item.ProgressBar + } + return widget.NewProgressBar() +} + +func (o QueueLayoutObject) Add(id int, queue *Queue) { + progressBar := widget.NewProgressBar() + statusMessage := canvas.NewText(o.getStatusTitle(queue.Status), theme.PrimaryColor()) + messageError := canvas.NewText("", theme.ErrorColor()) + + content := container.NewVBox( + container.NewHScroll(widget.NewLabel(queue.Setting.VideoFileInput.Name)), + progressBar, + container.NewHScroll(statusMessage), + container.NewHScroll(messageError), + canvas.NewLine(theme.FocusColor()), + container.NewPadded(), + ) + o.items[id] = QueueLayoutItem{ + CanvasObject: content, + ProgressBar: progressBar, + StatusMessage: statusMessage, + MessageError: messageError, + } + o.container.Add(content) +} + +func (o QueueLayoutObject) Remove(id int) { + if item, ok := o.items[id]; ok { + o.container.Remove(item.CanvasObject) + 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 + item.StatusMessage.Refresh() + if queue.Error != nil { + item.MessageError.Text = queue.Error.Error() + item.MessageError.Color = statusColor + item.MessageError.Refresh() + } + } +} + +func (o QueueLayoutObject) getStatusColor(status StatusContract) color.Color { + if status == StatusType(Error) { + return theme.ErrorColor() + } + + if status == StatusType(Completed) { + return color.RGBA{R: 49, G: 127, B: 114, A: 255} + } + + return theme.PrimaryColor() +} + +func (o QueueLayoutObject) getStatusTitle(status StatusContract) string { + return o.localizerService.GetMessage(&i18n.LocalizeConfig{MessageID: status.name()}) +} + +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 + 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 + 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 + p.progressbar.Refresh() + } + } + + if isProcessCompleted == false { + if len(errorText) == 0 { + errorText = p.localizerService.GetMessage(&i18n.LocalizeConfig{ + MessageID: "errorConverter", + }) + } + return errors.New(errorText) + } + + return nil +} + +type LayoutLocalizerItem struct { + messageID string + object *widget.Label +} + +type LayoutLocalizerListener struct { + itemCurrentId int + items map[int]*LayoutLocalizerItem +} + +type LayoutLocalizerListenerContract interface { + AddItem(messageID string, object *widget.Label) +} + +func NewLayoutLocalizerListener() *LayoutLocalizerListener { + return &LayoutLocalizerListener{ + itemCurrentId: 0, + items: map[int]*LayoutLocalizerItem{}, + } +} + +func (l LayoutLocalizerListener) AddItem(messageID string, object *widget.Label) { + l.itemCurrentId += 1 + l.items[l.itemCurrentId] = &LayoutLocalizerItem{messageID: messageID, object: object} +} + +func (l LayoutLocalizerListener) Change(localizerService LocalizerContract) { + for _, item := range l.items { + item.object.Text = localizerService.GetMessage(&i18n.LocalizeConfig{MessageID: item.messageID}) + item.object.Refresh() + } +} diff --git a/localizer/service.go b/kernel/localizer.go similarity index 62% rename from localizer/service.go rename to kernel/localizer.go index 1b27594..e002125 100644 --- a/localizer/service.go +++ b/kernel/localizer.go @@ -1,4 +1,4 @@ -package localizer +package kernel import ( "github.com/BurntSushi/toml" @@ -10,12 +10,17 @@ import ( "sort" ) -type ServiceContract interface { +type LocalizerContract interface { GetLanguages() []Lang GetMessage(localizeConfig *i18n.LocalizeConfig) string SetCurrentLanguage(lang Lang) error SetCurrentLanguageByCode(code string) error GetCurrentLanguage() *CurrentLanguage + AddListener(listener LocalizerListenerContract) +} + +type LocalizerListenerContract interface { + Change(localizerService LocalizerContract) } type Lang struct { @@ -29,13 +34,14 @@ type CurrentLanguage struct { localizerDefault *i18n.Localizer } -type Service struct { - bundle *i18n.Bundle - languages []Lang - currentLanguage *CurrentLanguage +type Localizer struct { + bundle *i18n.Bundle + languages []Lang + currentLanguage *CurrentLanguage + localizerListener map[int]LocalizerListenerContract } -func NewService(directory string, languageDefault language.Tag) (*Service, error) { +func NewLocalizer(directory string, languageDefault language.Tag) (*Localizer, error) { bundle := i18n.NewBundle(languageDefault) bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) @@ -46,7 +52,7 @@ func NewService(directory string, languageDefault language.Tag) (*Service, error localizerDefault := i18n.NewLocalizer(bundle, languageDefault.String()) - return &Service{ + return &Localizer{ bundle: bundle, languages: languages, currentLanguage: &CurrentLanguage{ @@ -57,6 +63,7 @@ func NewService(directory string, languageDefault language.Tag) (*Service, error localizer: localizerDefault, localizerDefault: localizerDefault, }, + localizerListener: map[int]LocalizerListenerContract{}, }, nil } @@ -81,14 +88,14 @@ func initLanguages(directory string, bundle *i18n.Bundle) ([]Lang, error) { return languages, nil } -func (s Service) GetLanguages() []Lang { - return s.languages +func (l Localizer) GetLanguages() []Lang { + return l.languages } -func (s Service) GetMessage(localizeConfig *i18n.LocalizeConfig) string { - message, err := s.GetCurrentLanguage().localizer.Localize(localizeConfig) +func (l Localizer) GetMessage(localizeConfig *i18n.LocalizeConfig) string { + message, err := l.GetCurrentLanguage().localizer.Localize(localizeConfig) if err != nil { - message, err = s.GetCurrentLanguage().localizerDefault.Localize(localizeConfig) + message, err = l.GetCurrentLanguage().localizerDefault.Localize(localizeConfig) if err != nil { return err.Error() } @@ -96,23 +103,34 @@ func (s Service) GetMessage(localizeConfig *i18n.LocalizeConfig) string { return message } -func (s Service) SetCurrentLanguage(lang Lang) error { - s.currentLanguage.Lang = lang - s.currentLanguage.localizer = i18n.NewLocalizer(s.bundle, lang.Code) +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 (s Service) SetCurrentLanguageByCode(code string) error { +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 s.SetCurrentLanguage(Lang{Code: lang.String(), Title: title}) + return l.SetCurrentLanguage(Lang{Code: lang.String(), Title: title}) } -func (s Service) GetCurrentLanguage() *CurrentLanguage { - return s.currentLanguage +func (l Localizer) GetCurrentLanguage() *CurrentLanguage { + return l.currentLanguage +} + +func (l Localizer) AddListener(listener LocalizerListenerContract) { + l.localizerListener[len(l.localizerListener)] = listener +} + +func (l Localizer) eventSetCurrentLanguage() { + for _, listener := range l.localizerListener { + listener.Change(l) + } } type languagesSort []Lang diff --git a/kernel/queue.go b/kernel/queue.go new file mode 100644 index 0000000..d820fa6 --- /dev/null +++ b/kernel/queue.go @@ -0,0 +1,137 @@ +package kernel + +import ( + "errors" +) + +type Queue struct { + Setting *ConvertSetting + Status StatusContract + Error error +} + +type File struct { + Path string + Name string + Ext string +} + +type ConvertSetting struct { + VideoFileInput File + VideoFileOut File + OverwriteOutputFiles bool +} + +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/window.go b/kernel/window.go new file mode 100644 index 0000000..d60971b --- /dev/null +++ b/kernel/window.go @@ -0,0 +1,67 @@ +package kernel + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/dialog" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/helper" +) + +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 + ShowAndRun() + GetLayout() LayoutContract +} + +type Window struct { + windowFyne fyne.Window + layout LayoutContract +} + +func newWindow(w fyne.Window, layout LayoutContract) Window { + w.Resize(fyne.Size{Width: 799, Height: 599}) + w.CenterOnScreen() + + return Window{ + windowFyne: w, + layout: layout, + } +} + +func (w Window) SetContent(content fyne.CanvasObject) { + 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 +} diff --git a/languages/active.en.toml b/languages/active.en.toml index 2ffafeb..499b140 100644 --- a/languages/active.en.toml +++ b/languages/active.en.toml @@ -38,6 +38,10 @@ other = "Allow file to be overwritten" hash = "sha1-f60bb5f761024d973834b5e9d25ceebce2c85f94" other = "choose" +[completed] +hash = "sha1-398c7d4f7b0d522afb930769c0fbb1a9f4b61fbe" +other = "Completed" + [converterVideoFilesSubmitTitle] hash = "sha1-7ac460f3c24c9952082f2db6e4d62f752598709c" other = "Convert" @@ -110,6 +114,10 @@ other = "File for conversion:" hash = "sha1-6a45cef900c668effcb2ab10da05855c1fd10f6f" other = "Help" +[inProgress] +hash = "sha1-eff79c40e2100ae5fadf3a7d99336025edcca8b5" +other = "In Progress" + [languageSelectionFormHead] hash = "sha1-0ff5fa82cf684112660128cba1711297acf11003" other = "Switch language" @@ -142,6 +150,10 @@ other = "Project website" hash = "sha1-fa2e4994a301bb24bc2a8fa166e5486ea95a7475" other = "**Program version:** {{.Version}}" +[queue] +hash = "sha1-aec93b16baeaf55fed871075c9494a460e4a91b8" +other = "Queue" + [save] hash = "sha1-4864057d626a868fa60f999bed3191d61d045ddc" other = "Save" @@ -165,3 +177,7 @@ other = "You can download it from here" [unzipRun] hash = "sha1-c554dad13026668a1f6adf3171837c5d51bbac36" other = "Unpacked..." + +[waiting] +hash = "sha1-307429dd84150877080c4bbff2b340d1e7dadff2" +other = "Waiting" diff --git a/languages/active.kk.toml b/languages/active.kk.toml index 990a3ad..d443d8a 100644 --- a/languages/active.kk.toml +++ b/languages/active.kk.toml @@ -38,6 +38,10 @@ other = "Файлды қайта жазуға рұқсат беріңіз" hash = "sha1-f60bb5f761024d973834b5e9d25ceebce2c85f94" other = "таңдау" +[completed] +hash = "sha1-398c7d4f7b0d522afb930769c0fbb1a9f4b61fbe" +other = "Дайын" + [converterVideoFilesSubmitTitle] hash = "sha1-7ac460f3c24c9952082f2db6e4d62f752598709c" other = "Файлды түрлендіру" @@ -110,6 +114,10 @@ other = "Түрлендіруге арналған файл:" hash = "sha1-6a45cef900c668effcb2ab10da05855c1fd10f6f" other = "Анықтама" +[inProgress] +hash = "sha1-eff79c40e2100ae5fadf3a7d99336025edcca8b5" +other = "Орындалуда" + [languageSelectionFormHead] hash = "sha1-0ff5fa82cf684112660128cba1711297acf11003" other = "Тілді ауыстыру" @@ -142,6 +150,10 @@ other = "Жобаның веб-сайты" hash = "sha1-fa2e4994a301bb24bc2a8fa166e5486ea95a7475" other = "**Бағдарлама нұсқасы:** {{.Version}}" +[queue] +hash = "sha1-aec93b16baeaf55fed871075c9494a460e4a91b8" +other = "Кезек" + [save] hash = "sha1-4864057d626a868fa60f999bed3191d61d045ddc" other = "Сақтау" @@ -165,3 +177,7 @@ other = "Сіз оны осы жерден жүктей аласыз" [unzipRun] hash = "sha1-c554dad13026668a1f6adf3171837c5d51bbac36" other = "Орамнан шығарылуда..." + +[waiting] +hash = "sha1-307429dd84150877080c4bbff2b340d1e7dadff2" +other = "Күту" diff --git a/languages/active.ru.toml b/languages/active.ru.toml index 7d94ea8..b163e1b 100644 --- a/languages/active.ru.toml +++ b/languages/active.ru.toml @@ -8,6 +8,7 @@ changeFFPath = "FFmpeg и FFprobe" changeLanguage = "Поменять язык" checkboxOverwriteOutputFilesTitle = "Разрешить перезаписать файл" choose = "выбрать" +completed = "Готово" converterVideoFilesSubmitTitle = "Конвертировать" converterVideoFilesTitle = "Конвертор видео файлов в mp4" download = "Скачать" @@ -26,6 +27,7 @@ ffmpegLGPL = "Это программное обеспечение исполь ffmpegTrademark = "**FFmpeg** — торговая марка **[Fabrice Bellard](http://bellard.org/)** , создателя проекта **[FFmpeg](https://ffmpeg.org/about.html)**." fileVideoForConversionTitle = "Файл для ковертации:" help = "Справка" +inProgress = "Выполняется" languageSelectionFormHead = "Переключить язык" languageSelectionHead = "Выберите язык" licenseLink = "Сведения о лицензии" @@ -34,9 +36,11 @@ pathToFfmpeg = "Путь к FFmpeg:" pathToFfprobe = "Путь к FFprobe:" programmLink = "Сайт проекта" programmVersion = "**Версия программы:** {{.Version}}" +queue = "Очередь" save = "Сохранить" selectFFPathTitle = "Укажите путь к FFmpeg и к FFprobe" settings = "Настройки" testFF = "Проверка FFmpeg на работоспособность..." titleDownloadLink = "Скачать можно от сюда" unzipRun = "Распаковывается..." +waiting = "В очереди" diff --git a/languages/translate.en.toml b/languages/translate.en.toml index 81cb622..acb16ac 100644 --- a/languages/translate.en.toml +++ b/languages/translate.en.toml @@ -1,3 +1,15 @@ -[testFF] -hash = "sha1-f5b8ed88e9609963035d2235be0a79bbec619976" -other = "Checking FFmpeg for serviceability..." +[completed] +hash = "sha1-398c7d4f7b0d522afb930769c0fbb1a9f4b61fbe" +other = "Completed" + +[inProgress] +hash = "sha1-eff79c40e2100ae5fadf3a7d99336025edcca8b5" +other = "In Progress" + +[queue] +hash = "sha1-aec93b16baeaf55fed871075c9494a460e4a91b8" +other = "Queue" + +[waiting] +hash = "sha1-307429dd84150877080c4bbff2b340d1e7dadff2" +other = "Waiting" diff --git a/languages/translate.kk.toml b/languages/translate.kk.toml index 8e64112..93b8581 100644 --- a/languages/translate.kk.toml +++ b/languages/translate.kk.toml @@ -1,3 +1,15 @@ -[testFF] -hash = "sha1-f5b8ed88e9609963035d2235be0a79bbec619976" -other = "FFmpeg функционалдығы тексерілуде..." +[completed] +hash = "sha1-398c7d4f7b0d522afb930769c0fbb1a9f4b61fbe" +other = "Дайын" + +[inProgress] +hash = "sha1-eff79c40e2100ae5fadf3a7d99336025edcca8b5" +other = "Орындалуда" + +[queue] +hash = "sha1-aec93b16baeaf55fed871075c9494a460e4a91b8" +other = "Кезек" + +[waiting] +hash = "sha1-307429dd84150877080c4bbff2b340d1e7dadff2" +other = "Күту" diff --git a/localizer/view.go b/localizer/view.go index 351d9c9..c0e5af6 100644 --- a/localizer/view.go +++ b/localizer/view.go @@ -3,27 +3,26 @@ package localizer 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 { - LanguageSelection(funcSelected func(lang Lang)) + LanguageSelection(funcSelected func(lang kernel.Lang)) } type View struct { - w fyne.Window - localizerService ServiceContract + app kernel.AppContract } -func NewView(w fyne.Window, localizerService ServiceContract) *View { +func NewView(app kernel.AppContract) *View { return &View{ - w: w, - localizerService: localizerService, + app: app, } } -func (v View) LanguageSelection(funcSelected func(lang Lang)) { - languages := v.localizerService.GetLanguages() +func (v View) LanguageSelection(funcSelected func(lang kernel.Lang)) { + languages := v.app.GetLocalizerService().GetLanguages() listView := widget.NewList( func() int { return len(languages) @@ -36,18 +35,17 @@ func (v View) LanguageSelection(funcSelected func(lang Lang)) { block.SetText(languages[i].Title) }) listView.OnSelected = func(id widget.ListItemID) { - _ = v.localizerService.SetCurrentLanguage(languages[id]) + _ = v.app.GetLocalizerService().SetCurrentLanguage(languages[id]) funcSelected(languages[id]) } - messageHead := v.localizerService.GetMessage(&i18n.LocalizeConfig{ + messageHead := v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "languageSelectionHead", }) - - v.w.SetContent(widget.NewCard(messageHead, "", listView)) + v.app.GetWindow().SetContent(widget.NewCard(messageHead, "", listView)) } -func LanguageSelectionForm(localizerService ServiceContract, funcSelected func(lang Lang)) fyne.CanvasObject { +func LanguageSelectionForm(localizerService kernel.LocalizerContract, funcSelected func(lang kernel.Lang)) fyne.CanvasObject { languages := localizerService.GetLanguages() currentLanguage := localizerService.GetCurrentLanguage() listView := widget.NewList( diff --git a/main.go b/main.go index 8b1dbc1..88a0e94 100644 --- a/main.go +++ b/main.go @@ -3,12 +3,10 @@ package main import ( "errors" "fyne.io/fyne/v2" - "fyne.io/fyne/v2/app" - "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/widget" "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" + "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/migration" @@ -20,36 +18,50 @@ import ( "os" ) -const appVersion string = "0.3.1" +var app kernel.AppContract +var ffPathUtilities *kernel.FFPathUtilities + +func init() { + iconResource, _ := fyne.LoadResourceFromPath("icon.png") + appMetadata := &fyne.AppMetadata{ + ID: "net.kor-elf.projects.gui-for-ffmpeg", + Name: "GUI for FFmpeg", + Version: "0.4.0", + Icon: iconResource, + } + + localizerService, err := kernel.NewLocalizer("languages", language.Russian) + if err != nil { + kernel.PanicErrorLang(err, appMetadata) + } + + ffPathUtilities = &kernel.FFPathUtilities{FFmpeg: "", FFprobe: ""} + convertorService := kernel.NewService(ffPathUtilities) + layoutLocalizerListener := kernel.NewLayoutLocalizerListener() + localizerService.AddListener(layoutLocalizerListener) + + queue := kernel.NewQueueList() + app = kernel.NewApp( + appMetadata, + localizerService, + queue, + kernel.NewQueueLayoutObject(queue, localizerService, layoutLocalizerListener), + convertorService, + ) +} func main() { - a := app.New() - iconResource, err := fyne.LoadResourceFromPath("icon.png") - if err == nil { - a.SetIcon(iconResource) - } - w := a.NewWindow("GUI for FFmpeg") - w.Resize(fyne.Size{Width: 800, Height: 600}) - w.CenterOnScreen() - - localizerService, err := localizer.NewService("languages", language.Russian) - if err != nil { - panicErrorLang(w, err) - w.ShowAndRun() - return - } - - errorView := error2.NewView(w, localizerService) + errorView := error2.NewView(app) if canCreateFile("data/database") != true { errorView.PanicErrorWriteDirectoryData() - w.ShowAndRun() + app.GetWindow().ShowAndRun() return } db, err := gorm.Open(sqlite.Open("data/database"), &gorm.Config{}) if err != nil { errorView.PanicError(err) - w.ShowAndRun() + app.GetWindow().ShowAndRun() return } @@ -58,7 +70,7 @@ func main() { err = migration.Run(db) if err != nil { errorView.PanicError(err) - w.ShowAndRun() + app.GetWindow().ShowAndRun() return } @@ -67,34 +79,37 @@ func main() { pathFFmpeg, err := convertorRepository.GetPathFfmpeg() if err != nil && errors.Is(err, gorm.ErrRecordNotFound) == false { errorView.PanicError(err) - w.ShowAndRun() + app.GetWindow().ShowAndRun() return } + ffPathUtilities.FFmpeg = pathFFmpeg + pathFFprobe, err := convertorRepository.GetPathFfprobe() if err != nil && errors.Is(err, gorm.ErrRecordNotFound) == false { errorView.PanicError(err) - w.ShowAndRun() + app.GetWindow().ShowAndRun() return } + ffPathUtilities.FFprobe = pathFFprobe - ffPathUtilities := convertor.FFPathUtilities{FFmpeg: pathFFmpeg, FFprobe: pathFFprobe} + app.RunConvertor() + defer app.AfterClosing() - localizerView := localizer.NewView(w, localizerService) - convertorView := convertor.NewView(w, localizerService) - convertorService := convertor.NewService(ffPathUtilities) - defer appCloseWithConvert(convertorService) - convertorHandler := handler.NewConvertorHandler(convertorService, convertorView, convertorRepository, localizerService) + localizerView := localizer.NewView(app) + convertorView := convertor.NewView(app) + convertorHandler := handler.NewConvertorHandler(app, convertorView, convertorRepository) localizerRepository := localizer.NewRepository(settingRepository) - menuView := menu.NewView(w, a, appVersion, localizerService) - mainMenu := handler.NewMenuHandler(convertorHandler, menuView, localizerService, localizerView, localizerRepository) + menuView := menu.NewView(app) + localizerListener := handler.NewLocalizerListener() + app.GetLocalizerService().AddListener(localizerListener) + mainMenu := handler.NewMenuHandler(app, convertorHandler, menuView, localizerView, localizerRepository, localizerListener) - mainHandler := handler.NewMainHandler(convertorHandler, mainMenu, localizerRepository, localizerService) + mainHandler := handler.NewMainHandler(app, convertorHandler, mainMenu, localizerRepository) mainHandler.Start() - w.SetMainMenu(mainMenu.GetMainMenu()) - - w.ShowAndRun() + app.GetWindow().SetMainMenu(mainMenu.GetMainMenu()) + app.GetWindow().ShowAndRun() } func appCloseWithDb(db *gorm.DB) { @@ -104,12 +119,6 @@ func appCloseWithDb(db *gorm.DB) { } } -func appCloseWithConvert(convertorService convertor.ServiceContract) { - for _, cmd := range convertorService.GetRunningProcesses() { - _ = cmd.Process.Kill() - } -} - func canCreateFile(path string) bool { file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0666) if err != nil { @@ -118,10 +127,3 @@ func canCreateFile(path string) bool { _ = file.Close() return true } - -func panicErrorLang(w fyne.Window, err error) { - w.SetContent(container.NewVBox( - widget.NewLabel("Произошла ошибка!"), - widget.NewLabel("произошла ошибка при получении языковых переводах. \n\r"+err.Error()), - )) -} diff --git a/menu/view.go b/menu/view.go index 0dc0896..f468498 100644 --- a/menu/view.go +++ b/menu/view.go @@ -5,7 +5,7 @@ import ( "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/widget" - "git.kor-elf.net/kor-elf/gui-for-ffmpeg/localizer" + "git.kor-elf.net/kor-elf/gui-for-ffmpeg/kernel" "github.com/nicksnyder/go-i18n/v2/i18n" "golang.org/x/image/colornames" "net/url" @@ -16,23 +16,17 @@ type ViewContract interface { } type View struct { - w fyne.Window - app fyne.App - appVersion string - localizerService localizer.ServiceContract + app kernel.AppContract } -func NewView(w fyne.Window, app fyne.App, appVersion string, localizerService localizer.ServiceContract) *View { +func NewView(app kernel.AppContract) *View { return &View{ - w: w, - app: app, - appVersion: appVersion, - localizerService: localizerService, + app: app, } } func (v View) About(ffmpegVersion string, ffprobeVersion string) { - view := v.app.NewWindow(v.localizerService.GetMessage(&i18n.LocalizeConfig{ + view := v.app.GetAppFyne().NewWindow(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "about", })) view.Resize(fyne.Size{Width: 793, Height: 550}) @@ -42,7 +36,7 @@ func (v View) About(ffmpegVersion string, ffprobeVersion string) { programmName.TextStyle = fyne.TextStyle{Bold: true} programmName.TextSize = 20 - programmLink := widget.NewHyperlink(v.localizerService.GetMessage(&i18n.LocalizeConfig{ + programmLink := widget.NewHyperlink(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "programmLink", }), &url.URL{ Scheme: "https", @@ -50,7 +44,7 @@ func (v View) About(ffmpegVersion string, ffprobeVersion string) { Path: "kor-elf/gui-for-ffmpeg/releases", }) - licenseLink := widget.NewHyperlink(v.localizerService.GetMessage(&i18n.LocalizeConfig{ + licenseLink := widget.NewHyperlink(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "licenseLink", }), &url.URL{ Scheme: "https", @@ -58,7 +52,7 @@ func (v View) About(ffmpegVersion string, ffprobeVersion string) { Path: "kor-elf/gui-for-ffmpeg/src/branch/main/LICENSE", }) - licenseLinkOther := widget.NewHyperlink(v.localizerService.GetMessage(&i18n.LocalizeConfig{ + licenseLinkOther := widget.NewHyperlink(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "licenseLinkOther", }), &url.URL{ Scheme: "https", @@ -66,16 +60,16 @@ func (v View) About(ffmpegVersion string, ffprobeVersion string) { Path: "kor-elf/gui-for-ffmpeg/src/branch/main/LICENSE-3RD-PARTY.txt", }) - programmVersion := widget.NewRichTextFromMarkdown(v.localizerService.GetMessage(&i18n.LocalizeConfig{ + programmVersion := widget.NewRichTextFromMarkdown(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "programmVersion", TemplateData: map[string]string{ - "Version": v.appVersion, + "Version": v.app.GetAppFyne().Metadata().Version, }, })) aboutText := widget.NewRichText( &widget.TextSegment{ - Text: v.localizerService.GetMessage(&i18n.LocalizeConfig{ + Text: v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "aboutText", }), }, @@ -84,10 +78,10 @@ func (v View) About(ffmpegVersion string, ffprobeVersion string) { image.SetMinSize(fyne.Size{Width: 100, Height: 100}) image.FillMode = canvas.ImageFillContain - ffmpegTrademark := widget.NewRichTextFromMarkdown(v.localizerService.GetMessage(&i18n.LocalizeConfig{ + ffmpegTrademark := widget.NewRichTextFromMarkdown(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "ffmpegTrademark", })) - ffmpegLGPL := widget.NewRichTextFromMarkdown(v.localizerService.GetMessage(&i18n.LocalizeConfig{ + ffmpegLGPL := widget.NewRichTextFromMarkdown(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "ffmpegLGPL", })) @@ -105,7 +99,7 @@ func (v View) About(ffmpegVersion string, ffprobeVersion string) { )), v.getAboutFfmpeg(ffmpegVersion), v.getAboutFfprobe(ffprobeVersion), - widget.NewCard(v.localizerService.GetMessage(&i18n.LocalizeConfig{ + widget.NewCard(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "AlsoUsedProgram", }), "", v.getOther()), )), @@ -123,7 +117,7 @@ func (v View) getAboutFfmpeg(version string) *fyne.Container { programmName.TextStyle = fyne.TextStyle{Bold: true} programmName.TextSize = 20 - programmLink := widget.NewHyperlink(v.localizerService.GetMessage(&i18n.LocalizeConfig{ + programmLink := widget.NewHyperlink(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "programmLink", }), &url.URL{ Scheme: "https", @@ -131,7 +125,7 @@ func (v View) getAboutFfmpeg(version string) *fyne.Container { Path: "", }) - licenseLink := widget.NewHyperlink(v.localizerService.GetMessage(&i18n.LocalizeConfig{ + licenseLink := widget.NewHyperlink(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "licenseLink", }), &url.URL{ Scheme: "https", @@ -153,7 +147,7 @@ func (v View) getAboutFfprobe(version string) *fyne.Container { programmName.TextStyle = fyne.TextStyle{Bold: true} programmName.TextSize = 20 - programmLink := widget.NewHyperlink(v.localizerService.GetMessage(&i18n.LocalizeConfig{ + programmLink := widget.NewHyperlink(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "programmLink", }), &url.URL{ Scheme: "https", @@ -161,7 +155,7 @@ func (v View) getAboutFfprobe(version string) *fyne.Container { Path: "ffprobe.html", }) - licenseLink := widget.NewHyperlink(v.localizerService.GetMessage(&i18n.LocalizeConfig{ + licenseLink := widget.NewHyperlink(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "licenseLink", }), &url.URL{ Scheme: "https", From bab8c1f3830d1ebaf67d8427fda524b2fa122be5 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sat, 17 Feb 2024 19:18:25 +0600 Subject: [PATCH 3/7] Bug fixed. When starting the program, sometimes the window was displayed incorrectly. --- kernel/window.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/kernel/window.go b/kernel/window.go index d60971b..47834d9 100644 --- a/kernel/window.go +++ b/kernel/window.go @@ -4,6 +4,7 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/dialog" "git.kor-elf.net/kor-elf/gui-for-ffmpeg/helper" + "time" ) type WindowContract interface { @@ -21,9 +22,14 @@ type Window struct { } func newWindow(w fyne.Window, layout LayoutContract) Window { - w.Resize(fyne.Size{Width: 799, Height: 599}) + w.Resize(fyne.Size{Width: 1039, Height: 599}) w.CenterOnScreen() + go func() { + time.Sleep(time.Millisecond * 500) + w.Resize(fyne.Size{Width: 1040, Height: 600}) + }() + return Window{ windowFyne: w, layout: layout, From 8e6fb904825a0601a04f7e5264281b37de9447b6 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sat, 17 Feb 2024 19:45:28 +0600 Subject: [PATCH 4/7] Code improvement. And added a comment to the code. --- kernel/window.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/kernel/window.go b/kernel/window.go index 47834d9..4d6d4c4 100644 --- a/kernel/window.go +++ b/kernel/window.go @@ -26,8 +26,15 @@ func newWindow(w fyne.Window, layout LayoutContract) Window { w.CenterOnScreen() go func() { + /** + * Bug fixed. + * When starting the program, sometimes the window was displayed incorrectly. + */ time.Sleep(time.Millisecond * 500) - w.Resize(fyne.Size{Width: 1040, Height: 600}) + size := w.Canvas().Size() + size.Width += 1 + size.Height += 1 + w.Resize(size) }() return Window{ From 0d05fdb30703bf82b20d752128f496ab13e339ed Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sat, 17 Feb 2024 19:48:50 +0600 Subject: [PATCH 5/7] Changed the screenshot in README.md. --- images/screenshot-gui-for-ffmpeg.png | Bin 31380 -> 46461 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/images/screenshot-gui-for-ffmpeg.png b/images/screenshot-gui-for-ffmpeg.png index 8f76a4242f197f76f726e64a4ced83e2f643c2e9..2183563904e30764ec1cdaa0b5dd5aa8e5f8e71c 100644 GIT binary patch literal 46461 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 literal 31380 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 From 240ae7aa96bc96db819e59f75ddfbd586326da0a Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sat, 17 Feb 2024 20:04:11 +0600 Subject: [PATCH 6/7] Windows OS fix. --- convertor/view_setting_button_download_ffmpeg_windows.go | 6 +++--- handler/convertor_windows.go | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/convertor/view_setting_button_download_ffmpeg_windows.go b/convertor/view_setting_button_download_ffmpeg_windows.go index a573284..2dcb47a 100644 --- a/convertor/view_setting_button_download_ffmpeg_windows.go +++ b/convertor/view_setting_button_download_ffmpeg_windows.go @@ -29,7 +29,7 @@ func (v View) blockDownloadFFmpeg( var buttonDownloadFFmpeg *widget.Button - buttonDownloadFFmpeg = widget.NewButton(v.localizerService.GetMessage(&i18n.LocalizeConfig{ + buttonDownloadFFmpeg = widget.NewButton(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "download", }), func() { buttonDownloadFFmpeg.Disable() @@ -42,13 +42,13 @@ func (v View) blockDownloadFFmpeg( buttonDownloadFFmpeg.Enable() }) - downloadFFmpegFromSiteMessage := v.localizerService.GetMessage(&i18n.LocalizeConfig{ + downloadFFmpegFromSiteMessage := v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "downloadFFmpegFromSite", }) return container.NewVBox( canvas.NewLine(colornames.Darkgreen), - widget.NewCard(v.localizerService.GetMessage(&i18n.LocalizeConfig{ + widget.NewCard(v.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "buttonDownloadFFmpeg", }), "", container.NewVBox( widget.NewRichTextFromMarkdown( diff --git a/handler/convertor_windows.go b/handler/convertor_windows.go index 14984fc..9ea6702 100644 --- a/handler/convertor_windows.go +++ b/handler/convertor_windows.go @@ -29,7 +29,7 @@ func (h ConvertorHandler) downloadFFmpeg(progressBar *widget.ProgressBar, progre return err } } - progressMessage.Text = h.localizerService.GetMessage(&i18n.LocalizeConfig{ + progressMessage.Text = h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "downloadRun", }) progressMessage.Refresh() @@ -38,7 +38,7 @@ func (h ConvertorHandler) downloadFFmpeg(progressBar *widget.ProgressBar, progre return err } - progressMessage.Text = h.localizerService.GetMessage(&i18n.LocalizeConfig{ + progressMessage.Text = h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "unzipRun", }) progressMessage.Refresh() @@ -48,7 +48,7 @@ func (h ConvertorHandler) downloadFFmpeg(progressBar *widget.ProgressBar, progre } _ = os.Remove("ffmpeg/ffmpeg.zip") - progressMessage.Text = h.localizerService.GetMessage(&i18n.LocalizeConfig{ + progressMessage.Text = h.app.GetLocalizerService().GetMessage(&i18n.LocalizeConfig{ MessageID: "testFF", }) progressMessage.Refresh() From d0539f5e90733a39f435883b487ef80cbe838af8 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sat, 17 Feb 2024 20:06:09 +0600 Subject: [PATCH 7/7] Fixed an error when building an assembly in fyne-cross. --- main.go | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/main.go b/main.go index 88a0e94..5b98be8 100644 --- a/main.go +++ b/main.go @@ -18,7 +18,7 @@ import ( "os" ) -var app kernel.AppContract +var application kernel.AppContract var ffPathUtilities *kernel.FFPathUtilities func init() { @@ -41,7 +41,7 @@ func init() { localizerService.AddListener(layoutLocalizerListener) queue := kernel.NewQueueList() - app = kernel.NewApp( + application = kernel.NewApp( appMetadata, localizerService, queue, @@ -51,17 +51,17 @@ func init() { } func main() { - errorView := error2.NewView(app) + errorView := error2.NewView(application) if canCreateFile("data/database") != true { errorView.PanicErrorWriteDirectoryData() - app.GetWindow().ShowAndRun() + application.GetWindow().ShowAndRun() return } db, err := gorm.Open(sqlite.Open("data/database"), &gorm.Config{}) if err != nil { errorView.PanicError(err) - app.GetWindow().ShowAndRun() + application.GetWindow().ShowAndRun() return } @@ -70,7 +70,7 @@ func main() { err = migration.Run(db) if err != nil { errorView.PanicError(err) - app.GetWindow().ShowAndRun() + application.GetWindow().ShowAndRun() return } @@ -79,7 +79,7 @@ func main() { pathFFmpeg, err := convertorRepository.GetPathFfmpeg() if err != nil && errors.Is(err, gorm.ErrRecordNotFound) == false { errorView.PanicError(err) - app.GetWindow().ShowAndRun() + application.GetWindow().ShowAndRun() return } ffPathUtilities.FFmpeg = pathFFmpeg @@ -87,29 +87,29 @@ func main() { pathFFprobe, err := convertorRepository.GetPathFfprobe() if err != nil && errors.Is(err, gorm.ErrRecordNotFound) == false { errorView.PanicError(err) - app.GetWindow().ShowAndRun() + application.GetWindow().ShowAndRun() return } ffPathUtilities.FFprobe = pathFFprobe - app.RunConvertor() - defer app.AfterClosing() + application.RunConvertor() + defer application.AfterClosing() - localizerView := localizer.NewView(app) - convertorView := convertor.NewView(app) - convertorHandler := handler.NewConvertorHandler(app, convertorView, convertorRepository) + localizerView := localizer.NewView(application) + convertorView := convertor.NewView(application) + convertorHandler := handler.NewConvertorHandler(application, convertorView, convertorRepository) localizerRepository := localizer.NewRepository(settingRepository) - menuView := menu.NewView(app) + menuView := menu.NewView(application) localizerListener := handler.NewLocalizerListener() - app.GetLocalizerService().AddListener(localizerListener) - mainMenu := handler.NewMenuHandler(app, convertorHandler, menuView, localizerView, localizerRepository, localizerListener) + application.GetLocalizerService().AddListener(localizerListener) + mainMenu := handler.NewMenuHandler(application, convertorHandler, menuView, localizerView, localizerRepository, localizerListener) - mainHandler := handler.NewMainHandler(app, convertorHandler, mainMenu, localizerRepository) + mainHandler := handler.NewMainHandler(application, convertorHandler, mainMenu, localizerRepository) mainHandler.Start() - app.GetWindow().SetMainMenu(mainMenu.GetMainMenu()) - app.GetWindow().ShowAndRun() + application.GetWindow().SetMainMenu(mainMenu.GetMainMenu()) + application.GetWindow().ShowAndRun() } func appCloseWithDb(db *gorm.DB) {