Browse Source

删除无效代码

master
VIVIMAN 3 years ago
parent
commit
c60010f9c7
  1. 27
      gui_wails03_learn/.gitignore
  2. 8
      gui_wails03_learn/.idea/.gitignore
  3. 21
      gui_wails03_learn/LICENSE
  4. 74
      gui_wails03_learn/README.md
  5. 725
      gui_wails03_learn/app.go
  6. BIN
      gui_wails03_learn/appicon.png
  7. 4
      gui_wails03_learn/build.bat
  8. 65
      gui_wails03_learn/clipboard/clipboard.go
  9. 52
      gui_wails03_learn/clipboard/clipboard_darwin.go
  10. 129
      gui_wails03_learn/clipboard/clipboard_unix.go
  11. 169
      gui_wails03_learn/clipboard/clipboard_windows.go
  12. 22
      gui_wails03_learn/constants/constants.go
  13. 12
      gui_wails03_learn/constants/errors.go
  14. 219
      gui_wails03_learn/db/db.go
  15. 17
      gui_wails03_learn/frontend/.browserslistrc
  16. 16
      gui_wails03_learn/frontend/.editorconfig
  17. 45
      gui_wails03_learn/frontend/.gitignore
  18. 27
      gui_wails03_learn/frontend/README.md
  19. 119
      gui_wails03_learn/frontend/angular.json
  20. 44
      gui_wails03_learn/frontend/karma.conf.js
  21. 26596
      gui_wails03_learn/frontend/package-lock.json
  22. 55
      gui_wails03_learn/frontend/package.json
  23. 1
      gui_wails03_learn/frontend/package.json.md5
  24. 28
      gui_wails03_learn/frontend/src/app/app-routing.module.ts
  25. 3
      gui_wails03_learn/frontend/src/app/app.component.html
  26. 0
      gui_wails03_learn/frontend/src/app/app.component.scss
  27. 35
      gui_wails03_learn/frontend/src/app/app.component.spec.ts
  28. 55
      gui_wails03_learn/frontend/src/app/app.component.ts
  29. 124
      gui_wails03_learn/frontend/src/app/app.module.ts
  30. 19
      gui_wails03_learn/frontend/src/app/common/custom-icons.ts
  31. 3
      gui_wails03_learn/frontend/src/app/common/fn.ts
  32. 40
      gui_wails03_learn/frontend/src/app/components/audio-player/audio-player.component.html
  33. 121
      gui_wails03_learn/frontend/src/app/components/audio-player/audio-player.component.scss
  34. 182
      gui_wails03_learn/frontend/src/app/components/audio-player/audio-player.component.ts
  35. 63
      gui_wails03_learn/frontend/src/app/components/audio-player/audio-player.service.ts
  36. 20
      gui_wails03_learn/frontend/src/app/components/confirmation-dialog/confirmation-dialog.component.html
  37. 21
      gui_wails03_learn/frontend/src/app/components/confirmation-dialog/confirmation-dialog.component.scss
  38. 47
      gui_wails03_learn/frontend/src/app/components/confirmation-dialog/confirmation-dialog.component.ts
  39. 7
      gui_wails03_learn/frontend/src/app/components/index.ts
  40. 9
      gui_wails03_learn/frontend/src/app/components/language-switcher/language-switcher.component.html
  41. 24
      gui_wails03_learn/frontend/src/app/components/language-switcher/language-switcher.component.scss
  42. 62
      gui_wails03_learn/frontend/src/app/components/language-switcher/language-switcher.component.ts
  43. 33
      gui_wails03_learn/frontend/src/app/components/loader/loader.component.html
  44. 107
      gui_wails03_learn/frontend/src/app/components/loader/loader.component.scss
  45. 62
      gui_wails03_learn/frontend/src/app/components/loader/loader.component.ts
  46. 39
      gui_wails03_learn/frontend/src/app/components/playlist/add-to-playlist.component.html
  47. 10
      gui_wails03_learn/frontend/src/app/components/playlist/add-to-playlist.component.scss
  48. 65
      gui_wails03_learn/frontend/src/app/components/playlist/add-to-playlist.component.ts
  49. 26
      gui_wails03_learn/frontend/src/app/components/playlist/create.component.html
  50. 10
      gui_wails03_learn/frontend/src/app/components/playlist/create.component.scss
  51. 47
      gui_wails03_learn/frontend/src/app/components/playlist/create.component.ts
  52. 2
      gui_wails03_learn/frontend/src/app/components/playlist/index.ts
  53. 173
      gui_wails03_learn/frontend/src/app/components/settings/settings.component.html
  54. 88
      gui_wails03_learn/frontend/src/app/components/settings/settings.component.scss
  55. 136
      gui_wails03_learn/frontend/src/app/components/settings/settings.component.ts
  56. 33
      gui_wails03_learn/frontend/src/app/components/updater/updater.component.html
  57. 33
      gui_wails03_learn/frontend/src/app/components/updater/updater.component.scss
  58. 47
      gui_wails03_learn/frontend/src/app/components/updater/updater.component.ts
  59. 14
      gui_wails03_learn/frontend/src/app/directives/autofocus.directive.ts
  60. 1
      gui_wails03_learn/frontend/src/app/directives/index.ts
  61. 82
      gui_wails03_learn/frontend/src/app/models/app-state.ts
  62. 9
      gui_wails03_learn/frontend/src/app/models/entry.ts
  63. 6
      gui_wails03_learn/frontend/src/app/models/index.ts
  64. 30
      gui_wails03_learn/frontend/src/app/models/ngrok.ts
  65. 5
      gui_wails03_learn/frontend/src/app/models/offline-playlist.ts
  66. 8
      gui_wails03_learn/frontend/src/app/models/playlist.ts
  67. 19
      gui_wails03_learn/frontend/src/app/models/track.ts
  68. 33
      gui_wails03_learn/frontend/src/app/models/update-release.ts
  69. 186
      gui_wails03_learn/frontend/src/app/pages/home/home.component.html
  70. 276
      gui_wails03_learn/frontend/src/app/pages/home/home.component.scss
  71. 760
      gui_wails03_learn/frontend/src/app/pages/home/home.component.ts
  72. 2
      gui_wails03_learn/frontend/src/app/pages/index.ts
  73. 36
      gui_wails03_learn/frontend/src/app/pages/offline-playlist/offline-playlist.component.html
  74. 133
      gui_wails03_learn/frontend/src/app/pages/offline-playlist/offline-playlist.component.scss
  75. 190
      gui_wails03_learn/frontend/src/app/pages/offline-playlist/offline-playlist.component.ts
  76. 38
      gui_wails03_learn/frontend/src/app/scss/breakpoints.scss
  77. 69
      gui_wails03_learn/frontend/src/app/services/loader.service.ts
  78. 57
      gui_wails03_learn/frontend/src/app/services/snackbar.service.ts
  79. 0
      gui_wails03_learn/frontend/src/assets/.gitkeep
  80. BIN
      gui_wails03_learn/frontend/src/assets/MaterialIcons-Regular.eot
  81. 1
      gui_wails03_learn/frontend/src/assets/MaterialIcons-Regular.ijmap
  82. 2373
      gui_wails03_learn/frontend/src/assets/MaterialIcons-Regular.svg
  83. BIN
      gui_wails03_learn/frontend/src/assets/MaterialIcons-Regular.ttf
  84. BIN
      gui_wails03_learn/frontend/src/assets/MaterialIcons-Regular.woff
  85. BIN
      gui_wails03_learn/frontend/src/assets/MaterialIcons-Regular.woff2
  86. BIN
      gui_wails03_learn/frontend/src/assets/bg.png
  87. 184
      gui_wails03_learn/frontend/src/assets/i18n/en.json
  88. 184
      gui_wails03_learn/frontend/src/assets/i18n/it.json
  89. 184
      gui_wails03_learn/frontend/src/assets/i18n/pl.json
  90. 3
      gui_wails03_learn/frontend/src/assets/icons/download-failed.svg
  91. 3
      gui_wails03_learn/frontend/src/assets/icons/github.svg
  92. 3
      gui_wails03_learn/frontend/src/assets/icons/qrcode-scan.svg
  93. 36
      gui_wails03_learn/frontend/src/assets/material-icons.css
  94. BIN
      gui_wails03_learn/frontend/src/assets/playing-wave.gif
  95. 387
      gui_wails03_learn/frontend/src/backend/index.js
  96. 11
      gui_wails03_learn/frontend/src/backend/package.json
  97. 3
      gui_wails03_learn/frontend/src/environments/environment.hmr.ts
  98. 3
      gui_wails03_learn/frontend/src/environments/environment.prod.ts
  99. 16
      gui_wails03_learn/frontend/src/environments/environment.ts
  100. BIN
      gui_wails03_learn/frontend/src/favicon.ico

27
gui_wails03_learn/.gitignore

@ -0,0 +1,27 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
build/
.version
pwa/
# System Files
.DS_Store
Thumbs.db
# Dependency directories (remove the comment below to include it)
# vendor/
/.idea/gui_wails03_learn.iml
/.idea/misc.xml
/.idea/modules.xml
/.idea/vcs.xml

8
gui_wails03_learn/.idea/.gitignore

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Datasource local storage ignored files
/../../../../../:\ViviCode\golangs_learn\gui_wails03_learn\.idea/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

21
gui_wails03_learn/LICENSE

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 marcio199226
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

74
gui_wails03_learn/README.md

@ -0,0 +1,74 @@
# ytd
Dekstop app for downloading audio tracks from youtube built with wails & angular
**(*) please note this is an alpha version in case of malfunction please open an issue**
## Install
[Download from releases page](https://github.com/marcio199226/ytd/releases)
[PWA](https://ytd.surge.sh/)
Supported platfroms for now
- Macos (tested on 11.5.x/11.6.x & 12.x apple m1)
## Main features:
- check for updates & app update
- clipboard watch (once yt link is copied it will be automatically downloaded, **can be disabled**)
- run in bg on close (run app in bg even if you closed app window)
- convert webm files to mp3
- system tray with fast settings
- in app player for single tracks and playlists (for now only for offline created playlists)
- Internalization
- English
- Polish
- Italian
- clean and simple UI (I hope ;))
- create offline playlist
- playback playlist
- add/remove tracks
- export to any external devices (pen drive , external hd etc...) or any folder
- Made with :green_heart: with golang & angular in my spare time
## Screenshots
<img width="1552" alt="home" title="Home" src="https://user-images.githubusercontent.com/10244404/136397513-8bde9053-0d1f-4257-83f5-71322e166cb1.png">
<img width="1552" alt="settings_dialog" title="Settings" src="https://user-images.githubusercontent.com/10244404/136397782-3962a502-0b26-4048-adc0-50e871f2e6f4.png">
<img width="1552" alt="downloading_track" title="Downloading track" src="https://user-images.githubusercontent.com/10244404/136397721-6ccebfaa-b7b7-4d53-94de-1cbdd0ac8ea3.png">
<img width="1552" alt="track_playback" title="Playback track" src="https://user-images.githubusercontent.com/10244404/136397829-eaae8d9f-f5a4-422d-aba1-a23bba2a2501.png">
<img width="1552" alt="playlist_playback" title="Playback playlist" src="https://user-images.githubusercontent.com/10244404/136397770-79cfdef2-8daf-4bd4-a1e4-3b969c2031f9.png">
## Build from sources
Wails requirements: https://wails.io/docs/gettingstarted/installation
#### Dev env
Angular
`cd frontend && npm install && npm run serve`
Wails
`wails dev --e "html"`
Open tab in chrome (preffered) and go to http://localhost:4200
Extract translation for golang side:
`xgotext -exclude "vendor,frontend" -in "/Users/oskarmarciniak/projects/golang/ytd" -out "/Users/oskarmarciniak/projects/golang/ytd/i18n"`
#### Build binaries (Macos only at the moment)
`wails build --platform darwin/arm64 --clean --package --production`
`wails build --platform darwin/amd64 --clean --package --production --upx`
(*) upx doesn't work for apple m1 https://github.com/upx/upx/issues/446
## Roadmap
- [ ] Chrome extension so tracks may be downloaded without user interaction (even without copy yt links)
- [x] Internalization
- [x] Share tracks through public webserver running locally
- [ ] Share tracks through telegram (user could subscribe to ytd bot and then will be able to send downloaded tracks to yourself telegram account)
- [ ] Download playlists from yt (exports them, search for playlist, playback playlist etc...)

725
gui_wails03_learn/app.go

@ -0,0 +1,725 @@
package main
import (
"context"
"errors"
"fmt"
"github.com/wailsapp/wails/v2/pkg/options/mac"
"log"
"net/http"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
"strings"
"time"
. "ytd/constants"
. "ytd/db"
. "ytd/models"
. "ytd/plugins"
"github.com/denisbrodbeck/machineid"
"github.com/leonelquinteros/gotext"
"github.com/mitchellh/mapstructure"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options/dialog"
"github.com/xujiajun/nutsdb"
)
var i18nPath string
var wailsRuntime *wails.Runtime
type HostInfo struct {
ID string `json:"id"`
Username string `json:"username"`
}
type AppState struct {
runtime *wails.Runtime
db *nutsdb.DB
plugins []Plugin
Entries []GenericEntry `json:"entries"`
OfflinePlaylists []OfflinePlaylist `json:"offlinePlaylists"`
Config *AppConfig `json:"config"`
Stats *AppStats `json:"stats"`
AppVersion string `json:"appVersion"`
Host HostInfo `json:"host"`
PwaUrl string `json:"pwaUrl"`
context.Context `json:"-"`
isInForeground bool
canStartAtLogin bool
tray TrayMenu
updater *Updater
offlinePlaylistService *OfflinePlaylistService
ngrok *NgrokService
convertQueue chan GenericEntry
}
func (state *AppState) PreWailsInit(ctx context.Context) {
state.db = InitializeDb()
state.Entries = DbGetAllEntries()
state.OfflinePlaylists = DbGetAllOfflinePlaylists()
state.offlinePlaylistService = &OfflinePlaylistService{}
state.ngrok = &NgrokService{}
state.ngrok.Context = ctx
state.Config = state.Config.Init()
state.AppVersion = version
state.convertQueue = make(chan GenericEntry)
// configure i18n paths
gotext.Configure("/Users/oskarmarciniak/projects/golang/ytd/i18n", state.Config.Language, "default")
if _, err := mac.StartsAtLogin(); err == nil {
state.canStartAtLogin = true
}
if machineid, err := machineid.ProtectedID("ytd"); err == nil {
state.Host.ID = machineid
}
if user, err := user.Current(); err == nil {
state.Host.Username = user.Username
}
}
func (state *AppState) WailsInit(runtime *wails.Runtime) {
// Save runtime
state.runtime = runtime
state.offlinePlaylistService.runtime = runtime
state.ngrok.runtime = runtime
state.Config.SetRuntime(runtime)
// Do some other initialisation
state.Stats = &AppStats{}
appState = state
// this is sync so it blocks until finished and wails:loaded are not dispatched until this finishes
if runtime.System.AppType() == "default" { // wails serve & ng serve
runtime.Events.On("wails:loaded", func(...interface{}) {
time.Sleep(100 * time.Millisecond)
fmt.Println("EMIT YTD:ONLOAD")
runtime.Events.Emit("ytd:onload", state)
})
} else { // dekstop build
go func() {
runtime.Events.Emit("ytd:onload", state)
}()
}
// initialize plugins
for _, plugin := range plugins {
plugin.SetWailsRuntime(runtime)
plugin.SetContext(state.Context)
plugin.SetAppConfig(state.Config)
plugin.SetAppStats(state.Stats)
}
fmt.Println("APP STATE INITIALIZED")
state.InitializeListeners()
// create app sys tray menu
state.tray.runtime = runtime
state.tray.createTray()
state.runtime.Menu.SetTrayMenu(state.tray.defaultTrayMenu)
// event emitted from fe if tray should be updated
runtime.Events.On("ytd:app:tray:update", func(data ...interface{}) {
state.tray.reRenderTray(func() {})
})
state.checkStartsAtLogin()
/* go func() {
for {
time.Sleep(10 * time.Second)
state.checkForTracksToDownload()
}
}() */
go func() {
ticker := time.NewTicker(3 * time.Second)
pending := make(chan int, maxConvertJobs)
for {
select {
case entry := <-state.convertQueue:
go func() {
if len(pending) == cap(pending) {
state.runtime.Events.Emit("ytd:track:convert:queued", entry)
}
pending <- 1
state.convertToMp3(ticker, &entry, true)
<-pending
}()
case <-ticker.C:
// get entries that could be converted
for _, t := range DbGetAllEntries() {
entry := t
plugin := getPluginFor(entry.Source)
if entry.Type == "track" && entry.Track.Status == TrackStatusDownladed && !entry.Track.IsConvertedToMp3 && plugin.IsTrackFileExists(entry.Track, "webm") {
// skip tracks which has failed at least 3 times in a row
if entry.Track.ConvertingStatus.Attempts >= 3 {
fmt.Printf("Skipping audio extraction for %s(%s)...due to too many attempts\n", entry.Track.Name, entry.Track.ID)
continue
}
go func() {
// this will block until pending channel is full
pending <- 1
state.convertToMp3(ticker, &entry, false)
<-pending
}()
}
}
default:
fmt.Printf("Convert to mp3 nothing to do....max parralel %d | converting %d | pending %d entries\n\n", maxConvertJobs, state.Stats.ConvertingCount, len(pending))
time.Sleep(1 * time.Second)
}
}
}()
go func() {
time.Sleep(10 * time.Second)
for {
state.checkForUpdates()
// check again in twelve hours
time.Sleep(12 * time.Hour)
}
}()
if state.Config.PublicServer.Enabled {
ShowLoader(state.runtime, "Starting public server...")
result := state.ngrok.StartProcess(false)
if result.err != nil {
state.runtime.Events.Emit("ytd:ngrok", NgrokStateEventPayload{Status: NgrokStatusError, ErrCode: result.errCode})
HideLoader(state.runtime)
return
}
state.runtime.Events.Emit("ytd:ngrok", NgrokStateEventPayload{Status: result.status, Url: result.publicUrl})
HideLoader(state.runtime)
// monitor ngrok state
go state.ngrok.MonitorNgrokProcess()
}
}
func (state *AppState) WailsShutdown() {
err := state.ngrok.KillProcess()
if err != nil {
fmt.Println("WailsShutdown state.ngrok.KillProcess() failed", err)
}
err = state.db.Merge()
if err != nil {
fmt.Println("WailsShutdown db.Merge() failed", err)
}
CloseDb()
}
func (state *AppState) ReloadNewLanguage() {
gotext.Configure(i18nPath, state.Config.Language, "default")
// re render tray to take effect for new language translations
state.tray.reRenderTray(func() {})
// do other stuff if needed to reload translations from some ui native elements
}
func (state *AppState) InitializeListeners() {
state.runtime.Events.On("ytd:app:foreground", func(optionalData ...interface{}) {
var json map[string]interface{} = optionalData[0].(map[string]interface{})
if isInForeground, ok := json["isInForeground"]; ok {
state.isInForeground = isInForeground.(bool)
}
})
state.runtime.Events.On("ytd:offline:playlists:addedTrack", func(optionalData ...interface{}) {
state.OfflinePlaylists, _ = state.offlinePlaylistService.GetPlaylists(true)
})
state.runtime.Events.On("ytd:offline:playlists:removedTrack", func(optionalData ...interface{}) {
state.OfflinePlaylists, _ = state.offlinePlaylistService.GetPlaylists(true)
})
state.runtime.Events.On("ytd:offline:playlists:created", func(optionalData ...interface{}) {
state.OfflinePlaylists, _ = state.offlinePlaylistService.GetPlaylists(true)
})
state.runtime.Events.On("ytd:offline:playlists:removed", func(optionalData ...interface{}) {
state.OfflinePlaylists, _ = state.offlinePlaylistService.GetPlaylists(true)
})
state.runtime.Events.On("ngrok:configured", func(optionalData ...interface{}) {
if state.Config.PublicServer.Enabled {
ShowLoader(state.runtime, "Configuring public server...")
result := state.ngrok.StartProcess(true)
if result.err != nil {
state.runtime.Events.Emit("ytd:ngrok", NgrokStateEventPayload{Status: NgrokStatusError, ErrCode: result.errCode})
HideLoader(state.runtime)
return
}
state.runtime.Events.Emit("ytd:ngrok", NgrokStateEventPayload{Status: result.status, Url: result.publicUrl})
HideLoader(state.runtime)
// monitor ngrok process
go state.ngrok.MonitorNgrokProcess()
}
if !state.Config.PublicServer.Enabled {
err := state.ngrok.KillProcess()
if err != nil {
SendNotification(state.runtime, NotificationEventPayload{Type: "error", Label: "Cannot shutdown public server"}, state.isInForeground)
}
}
})
}
func (state *AppState) GetAppConfig() *AppConfig {
return state.Config
}
func (state *AppState) SelectDirectory() (string, error) {
selectedDirectory, err := state.runtime.Dialog.OpenDirectory(&dialog.OpenDialog{
AllowFiles: false,
CanCreateDirectories: true,
AllowDirectories: true,
Title: gotext.Get("Choose directory"),
})
return selectedDirectory, err
}
func (state *AppState) GetEntryById(entry GenericEntry) *GenericEntry {
for _, t := range state.Entries {
if t.Type == "track" && t.Track.ID == entry.Track.ID {
return &t
}
}
return nil
}
func (state *AppState) checkForTracksToDownload() error {
fmt.Printf("Check for tracks to start downloads...%d/%d\n", state.Stats.DownloadingCount, state.Config.MaxParrallelDownloads)
if state.Stats.DownloadingCount >= state.Config.MaxParrallelDownloads {
return nil
}
fmt.Printf("Checking...%d entries\n", len(state.Entries))
// range over DbGetAllEntries()
for _, t := range DbGetAllEntries() {
var freeSlots uint = state.Config.MaxParrallelDownloads - state.Stats.DownloadingCount
entry := t
// auto download only tracks with processing status
// if track has pending/failed status it means that something goes wrong so user have to download it manually from UI
if entry.Type == "track" && entry.Track.Status == TrackStatusProcessing {
if freeSlots == 0 {
return nil
}
// start download for track
fmt.Printf("Found %s to download\n", entry.Track.Name)
plugin := getPluginFor(entry.Source)
// make chan GenericEntry
// goriutine scrive li dentro
if plugin != nil {
freeSlots--
go func(entry GenericEntry) {
// storedEntry := state.GetEntryById(entry)
// fmt.Printf("Stored entry %v\n", storedEntry)
// fmt.Println(entry.Track.Url)
// fmt.Println(storedEntry.Track.Url)
plugin.StartDownload(&entry)
// storedEntry.Track = entry.Track
}(entry)
}
}
}
// qui un for che legge dal channel e ogni volta che riceve una entry downloadata
return nil
}
func (state *AppState) convertToMp3(restartTicker *time.Ticker, entry *GenericEntry, force bool) error {
if !state.Config.ConvertToMp3 {
// if option is not enabled restart check after 3s
restartTicker.Stop()
restartTicker.Reset(3 * time.Second)
return nil
}
fmt.Println("Converting....")
ffmpeg, _ := state.IsFFmpegInstalled()
plugin := getPluginFor(entry.Source)
if entry.Type == "track" && entry.Track.Status == TrackStatusDownladed && !entry.Track.IsConvertedToMp3 && plugin.IsTrackFileExists(entry.Track, "webm") {
// skip tracks which has failed at least 3 times in a row
if entry.Track.ConvertingStatus.Attempts >= 3 && !force {
fmt.Printf("Skipping audio extraction for %s - (%s)...due to too many attempts\n", entry.Track.Name, entry.Track.ID)
return nil
}
fmt.Printf("Extracting audio for %s - (%s)...\n", entry.Track.Name, entry.Track.ID)
entry.Track.ConvertingStatus.Status = TrakcConverting
DbWriteEntry(entry.Track.ID, entry)
state.Stats.IncConvertCount()
state.runtime.Events.Emit("ytd:track", entry)
// ffmpeg -i "41qC3w3UUkU.webm" -vn -ab 128k -ar 44100 -y "41qC3w3UUkU.mp3"
outputPath := fmt.Sprintf("%s/%s.mp3", plugin.GetDir(), entry.Track.ID)
cmd := exec.CommandContext(
state.Context,
ffmpeg,
"-loglevel", "quiet",
"-i", fmt.Sprintf("%s/%s.webm", plugin.GetDir(), entry.Track.ID),
"-vn",
"-ab", "128k",
"-ar", "44100",
"-y", outputPath,
)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Println("Failed to extract audio:", err)
entry.Track.ConvertingStatus.Status = TrakcConvertFailed
entry.Track.ConvertingStatus.Err = err.Error()
entry.Track.ConvertingStatus.Attempts += 1
DbWriteEntry(entry.Track.ID, entry)
state.Stats.DecConvertCount()
state.runtime.Events.Emit("ytd:track", entry)
} else {
entry.Track.ConvertingStatus.Status = TrakcConverted
entry.Track.IsConvertedToMp3 = true
// check new filesize and save it
fileInfo, err := os.Stat(outputPath)
if err == nil {
entry.Track.ConvertingStatus.Filesize = int(fileInfo.Size())
}
DbWriteEntry(entry.Track.ID, entry)
state.Stats.DecConvertCount()
state.runtime.Events.Emit("ytd:track", entry)
// remove webm if needed
if state.Config.CleanWebmFiles && plugin.IsTrackFileExists(entry.Track, "webm") {
err = os.Remove(fmt.Sprintf("%s/%s.webm", plugin.GetDir(), entry.Track.ID))
if err != nil && !os.IsNotExist(err) {
fmt.Printf("Cannot remove %s.webm file after successfull converting to mp3\n", entry.Track.ID)
}
}
}
return nil
}
return nil
}
func (state *AppState) checkForUpdates() {
if !state.Config.CheckForUpdates {
return
}
state.updater = &Updater{
CurrentVersion: version,
LatestReleaseGitHubEndpoint: "https://api.github.com/repos/marcio199226/ytd/releases",
Client: &http.Client{Timeout: 10 * time.Minute},
SelectAsset: func(release Release, asset Asset) bool {
// look for the zip file
return strings.Contains(asset.Name, fmt.Sprintf("%s-%s", runtime.GOOS, runtime.GOARCH)) && filepath.Ext(asset.Name) == ".zip"
},
DownloadBytesLimit: 10_741_824, // 10MB
}
latest, hasUpdate, err := state.updater.HasUpdate()
if err != nil {
_, err := state.runtime.Dialog.Message(&dialog.MessageDialog{
Type: dialog.ErrorDialog,
Title: gotext.Get("Update check failed"),
Message: err.Error(),
Buttons: []string{"OK"},
CancelButton: "OK",
})
if err != nil {
log.Println(err)
return
}
return
}
if !hasUpdate {
_, err := state.runtime.Dialog.Message(&dialog.MessageDialog{
Type: dialog.InfoDialog,
Title: gotext.Get("You're up to date"),
Message: fmt.Sprintf(gotext.Get("%s is the latest version.", latest.TagName)),
Buttons: []string{"OK"},
CancelButton: "OK",
})
if err != nil {
log.Println(err)
return
}
return
}
clickedAction, _ := state.runtime.Dialog.Message(&dialog.MessageDialog{
Type: dialog.InfoDialog,
Title: fmt.Sprintf(gotext.Get("New version available: %s", latest.TagName)),
Message: gotext.Get("Would you like to update?"),
Buttons: []string{"OK", "Changelog", gotext.Get("Update")},
CancelButton: "OK",
})
state.tray.reRenderTray(func() {
state.tray.versionMenuItem.Label = fmt.Sprintf(gotext.Get("⚠️ ytd (%s) (new version %s)", version, latest.TagName))
})
switch clickedAction {
case "Update":
// continue
case "Changelog":
state.runtime.Window.Show()
state.runtime.Events.Emit("ytd:app:update:changelog", latest)
return
case "OK":
state.runtime.Events.Emit("ytd:app:update:available", latest)
return
}
state.Update(false)
}
func (state *AppState) Update(restart bool) {
_, err := state.updater.Update()
if err != nil {
_, err := state.runtime.Dialog.Message(&dialog.MessageDialog{
Type: dialog.WarningDialog,
Title: gotext.Get("Update not successful"),
Message: err.Error(),
Buttons: []string{"OK"},
CancelButton: "OK",
})
if err != nil {
log.Println(err)
return
}
}
// restart for now does not work properly, it closes current ytd instance but not launch the new one
// so notify user that update has been done and could restart app
// err = u.Restart()
state.runtime.Dialog.Message(&dialog.MessageDialog{
Type: dialog.InfoDialog,
Title: gotext.Get("Update successful"),
Message: gotext.Get("Please restart ytd for the changes to take effect."),
Buttons: []string{"OK"},
CancelButton: "OK",
})
}
func (state *AppState) checkStartsAtLogin() {
startsAtLogin, err := mac.StartsAtLogin()
if err != nil {
state.tray.reRenderTray(func() {
state.tray.startAtLoginMenuItem.Label = gotext.Get("⚠ Start at Login unavailable")
state.tray.startAtLoginMenuItem.Disabled = true
})
} else if startsAtLogin {
mac.ShowNotification("Ytd", gotext.Get("App has been started in background"), "", "")
state.tray.reRenderTray(func() {
state.tray.startAtLoginMenuItem.Checked = true
state.tray.startAtLoginMenuItem.Disabled = false
})
}
}
func (state *AppState) SaveSettingBoolValue(name string, val bool) (err error) {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovering from panic saveSettingValue:", r)
switch x := r.(type) {
case string:
err = errors.New(x)
case error:
err = x
default:
// Fallback err (per specs, error strings should be lowercase w/o punctuation
err = errors.New("unknown panic")
}
}
}()
error := DbSaveSettingBoolValue(name, val)
if err != nil {
return error
}
appState.Config.Set(name, val)
return nil
}
func (state *AppState) SaveSettingValue(name string, val string) (err error) {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovering from panic saveSettingValue:", r)
switch x := r.(type) {
case string:
err = errors.New(x)
case error:
err = x
default:
// Fallback err (per specs, error strings should be lowercase w/o punctuation
err = errors.New("unknown panic")
}
}
}()
error := DbWriteSetting(name, val)
if err != nil {
return error
}
appState.Config.Set(name, val)
return nil
}
func (state *AppState) ReadSettingBoolValue(name string) (bool, error) {
return DbReadSettingBoolValue(name)
}
func (state *AppState) ReadSettingValue(name string) (string, error) {
return DbReadSetting(name)
}
func (state *AppState) RemoveEntry(record map[string]interface{}) error {
var err error
var entry GenericEntry
err = mapstructure.Decode(record, &entry)
if err != nil {
return err
}
if entry.Type == "track" {
if err = DbDeleteEntry(entry.Track.ID); err == nil {
plugin := getPluginFor(entry.Source)
if plugin.IsTrackFileExists(entry.Track, "webm") {
err = os.Remove(fmt.Sprintf("%s/%s.webm", plugin.GetDir(), entry.Track.ID))
if err != nil && !os.IsNotExist(err) {
return err
}
}
// remove mp3 if file has been already converted
if plugin.IsTrackFileExists(entry.Track, "mp3") {
err = os.Remove(fmt.Sprintf("%s/%s.mp3", plugin.GetDir(), entry.Track.ID))
if err != nil && !os.IsNotExist(err) {
return err
}
}
// remove track from playlists
for _, p := range state.OfflinePlaylists {
for _, tid := range p.TracksIds {
if tid == entry.Track.ID {
state.offlinePlaylistService.RemoveTrackFromPlaylist(tid, p)
}
}
}
state.OfflinePlaylists, _ = state.offlinePlaylistService.GetPlaylists(true)
return nil
}
}
return err
}
func (state *AppState) AddToDownload(url string, isFromClipboard bool) error {
for _, plugin := range plugins {
if support := plugin.Supports(url); support {
if appState.GetAppConfig().ConcurrentDownloads {
go func() {
newEntries <- plugin.Fetch(url, isFromClipboard)
}()
} else {
newEntries <- plugin.Fetch(url, isFromClipboard)
}
continue
}
}
return nil
}
func (state *AppState) StartDownload(record map[string]interface{}) error {
var err error
var entry GenericEntry
err = mapstructure.Decode(record, &entry)
if err != nil {
return err
}
if appState.Stats.DownloadingCount >= appState.Config.MaxParrallelDownloads {
return errors.New("Max simultaneous downloads are reached please retry after some track finished downloading")
}
for _, plugin := range plugins {
if plugin.GetName() == entry.Source && entry.Type == "track" {
if appState.GetAppConfig().ConcurrentDownloads {
go func() {
plugin.StartDownload(&entry)
}()
} else {
plugin.StartDownload(&entry)
}
continue
}
}
return nil
}
func (state *AppState) AddToConvertQueue(entry GenericEntry) error {
state.convertQueue <- entry
return nil
}
func (state *AppState) IsSupportedUrl(url string) bool {
for _, plugin := range plugins {
if support := plugin.Supports(url); support {
return true
}
}
return false
}
func (state *AppState) IsFFmpegInstalled() (string, error) {
ffmpeg, err := exec.LookPath("ffmpeg")
if state.runtime.System.Platform() == "darwin" && err != nil {
// on darwin check if ffmpeg is maybe installed by homebrew
// (searching for ffmpeg only give wrong results if installed with homebrew)
ffmpeg, err := exec.LookPath("/opt/homebrew/bin/ffmpeg")
return ffmpeg, err
}
return ffmpeg, err
}
func (state *AppState) OpenUrl(url string) error {
return state.runtime.Browser.Open(url)
}
func (state *AppState) ForceQuit() {
state.runtime.Quit()
}
func (state *AppState) ShowWindow() {
state.runtime.Window.Show()
}
func getPluginFor(name string) Plugin {
for _, plugin := range plugins {
if plugin.GetName() == name {
return plugin
}
}
return nil
}
//WailsRuntime .
type WailsRuntime struct {
runtime *wails.Runtime
}

BIN
gui_wails03_learn/appicon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

4
gui_wails03_learn/build.bat

@ -0,0 +1,4 @@
# go build -ldflags="-s -w -H=windowsgui" main.go
cd frontend
npm install && npm run build && cd ../ && wails build

65
gui_wails03_learn/clipboard/clipboard.go

@ -0,0 +1,65 @@
package clipboard
import (
"context"
"fmt"
"sync"
"time"
)
type ErrFormatNotSupported struct {
msg string
formatType int
}
func (e *ErrFormatNotSupported) Error() string {
return fmt.Sprint("This format is unsupported")
}
// ReadAll read string from clipboard
func ReadAll() (string, error) {
return readAll()
}
// WriteAll write string to clipboard
func WriteAll(text string) error {
return writeAll(text)
}
// Unsupported might be set true during clipboard init, to help callers decide
// whether or not to offer clipboard options.
var Unsupported bool
// Monitor starts monitoring the clipboard for changes. When
// a change is detected, it is sent over the channel.
func MonitorClipboard(interval time.Duration, ctx context.Context, wg *sync.WaitGroup, stopCh <-chan struct{}, changes chan<- string) error {
currentValue, err := ReadAll()
if err != nil {
if _, ok := err.(*ErrFormatNotSupported); ok {
fmt.Println(err)
} else {
return err
}
}
for {
select {
case <-ctx.Done():
fmt.Println("Monitor exiting...")
close(changes)
time.Sleep(time.Millisecond * 10)
wg.Done()
return nil
default:
if newValue, err := ReadAll(); err == nil {
if newValue != currentValue {
currentValue = newValue
changes <- currentValue
}
} else {
fmt.Println(err)
}
}
time.Sleep(interval)
}
}

52
gui_wails03_learn/clipboard/clipboard_darwin.go

@ -0,0 +1,52 @@
// Copyright 2013 @atotto. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build darwin
package clipboard
import (
"os/exec"
)
var (
pasteCmdArgs = "pbpaste"
copyCmdArgs = "pbcopy"
)
func getPasteCommand() *exec.Cmd {
return exec.Command(pasteCmdArgs)
}
func getCopyCommand() *exec.Cmd {
return exec.Command(copyCmdArgs)
}
func readAll() (string, error) {
pasteCmd := getPasteCommand()
out, err := pasteCmd.Output()
if err != nil {
return "", err
}
return string(out), nil
}
func writeAll(text string) error {
copyCmd := getCopyCommand()
in, err := copyCmd.StdinPipe()
if err != nil {
return err
}
if err := copyCmd.Start(); err != nil {
return err
}
if _, err := in.Write([]byte(text)); err != nil {
return err
}
if err := in.Close(); err != nil {
return err
}
return copyCmd.Wait()
}

129
gui_wails03_learn/clipboard/clipboard_unix.go

@ -0,0 +1,129 @@
// Copyright 2013 @atotto. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build freebsd linux netbsd openbsd solaris dragonfly
package clipboard
import (
"errors"
"os"
"os/exec"
)
const (
xsel = "xsel"
xclip = "xclip"
wlcopy = "wl-copy"
wlpaste = "wl-paste"
termuxClipboardGet = "termux-clipboard-get"
termuxClipboardSet = "termux-clipboard-set"
)
var (
Primary bool
pasteCmdArgs []string
copyCmdArgs []string
xselPasteArgs = []string{xsel, "--output", "--clipboard"}
xselCopyArgs = []string{xsel, "--input", "--clipboard"}
xclipPasteArgs = []string{xclip, "-out", "-selection", "clipboard"}
xclipCopyArgs = []string{xclip, "-in", "-selection", "clipboard"}
wlpasteArgs = []string{wlpaste, "--no-newline"}
wlcopyArgs = []string{wlcopy}
termuxPasteArgs = []string{termuxClipboardGet}
termuxCopyArgs = []string{termuxClipboardSet}
missingCommands = errors.New("No clipboard utilities available. Please install xsel, xclip, wl-clipboard or Termux:API add-on for termux-clipboard-get/set.")
)
func init() {
if os.Getenv("WAYLAND_DISPLAY") != "" {
pasteCmdArgs = wlpasteArgs
copyCmdArgs = wlcopyArgs
if _, err := exec.LookPath(wlcopy); err == nil {
if _, err := exec.LookPath(wlpaste); err == nil {
return
}
}
}
pasteCmdArgs = xclipPasteArgs
copyCmdArgs = xclipCopyArgs
if _, err := exec.LookPath(xclip); err == nil {
return
}
pasteCmdArgs = xselPasteArgs
copyCmdArgs = xselCopyArgs
if _, err := exec.LookPath(xsel); err == nil {
return
}
pasteCmdArgs = termuxPasteArgs
copyCmdArgs = termuxCopyArgs
if _, err := exec.LookPath(termuxClipboardSet); err == nil {
if _, err := exec.LookPath(termuxClipboardGet); err == nil {
return
}
}
Unsupported = true
}
func getPasteCommand() *exec.Cmd {
if Primary {
pasteCmdArgs = pasteCmdArgs[:1]
}
return exec.Command(pasteCmdArgs[0], pasteCmdArgs[1:]...)
}
func getCopyCommand() *exec.Cmd {
if Primary {
copyCmdArgs = copyCmdArgs[:1]
}
return exec.Command(copyCmdArgs[0], copyCmdArgs[1:]...)
}
func readAll() (string, error) {
if Unsupported {
return "", missingCommands
}
pasteCmd := getPasteCommand()
out, err := pasteCmd.Output()
if err != nil {
return "", err
}
return string(out), nil
}
func writeAll(text string) error {
if Unsupported {
return missingCommands
}
copyCmd := getCopyCommand()
in, err := copyCmd.StdinPipe()
if err != nil {
return err
}
if err := copyCmd.Start(); err != nil {
return err
}
if _, err := in.Write([]byte(text)); err != nil {
return err
}
if err := in.Close(); err != nil {
return err
}
return copyCmd.Wait()
}

169
gui_wails03_learn/clipboard/clipboard_windows.go

@ -0,0 +1,169 @@
// +build windows
package clipboard
import (
"fmt"
"syscall"
"time"
"unsafe"
)
var (
user32 = syscall.MustLoadDLL("user32")
openClipboard = user32.MustFindProc("OpenClipboard")
closeClipboard = user32.MustFindProc("CloseClipboard")
emptyClipboard = user32.MustFindProc("EmptyClipboard")
getClipboardData = user32.MustFindProc("GetClipboardData")
setClipboardData = user32.MustFindProc("SetClipboardData")
setClipboardViewer = user32.MustFindProc("SetClipboardViewer")
CountClipboardFormats = user32.MustFindProc("CountClipboardFormats")
EnumClipboardFormats = user32.MustFindProc("EnumClipboardFormats")
GetClipboardFormatNameW = user32.MustFindProc("GetClipboardFormatNameW")
kernel32 = syscall.NewLazyDLL("kernel32")
globalAlloc = kernel32.NewProc("GlobalAlloc")
globalFree = kernel32.NewProc("GlobalFree")
globalLock = kernel32.NewProc("GlobalLock")
globalUnlock = kernel32.NewProc("GlobalUnlock")
lstrcpy = kernel32.NewProc("lstrcpyW")
)
const gmemMoveable = 0x0002
const (
CF_UNICODETEXT = 13
CF_TEXT = 1
CF_BITMAP = 2
CF_OEMTEXT = 7
CF_DIB = 8
CF_DIBV5 = 17
)
var supportedFormats [4]int = [4]int{CF_UNICODETEXT, CF_TEXT, CF_OEMTEXT}
func isSupportedFormat(format int) bool {
for i := 0; i < len(supportedFormats); i++ {
if supportedFormats[i] == format {
return true
}
}
return false
}
// waitOpenClipboard opens the clipboard, waiting for up to a second to do so.
func waitOpenClipboard() error {
started := time.Now()
limit := started.Add(time.Second)
var r uintptr
var error error
for time.Now().Before(limit) {
r, _, error = openClipboard.Call(0)
if r != 0 {
return nil
}
time.Sleep(time.Millisecond)
}
return error
}
func readAll() (string, error) {
err := waitOpenClipboard()
if err != nil {
return "", err
}
defer closeClipboard.Call()
formatsCount, _, _ := CountClipboardFormats.Call(0)
fmt.Printf("Available formats: %d\n", formatsCount)
availableFormat, _, _ := EnumClipboardFormats.Call(0)
fmt.Print("Available content formats: ")
hasSupportedFormat := false
for i := 0; i < (int)(formatsCount); i++ {
index := 0
if i > 0 {
index = i
}
format, _, _ := EnumClipboardFormats.Call(uintptr(index))
if format != 0 {
if isSupported := isSupportedFormat((int)(format)); isSupported {
hasSupportedFormat = true
break
}
}
}
if !hasSupportedFormat {
return "", &ErrFormatNotSupported{formatType: (int)(availableFormat)}
}
h, _, err := getClipboardData.Call(CF_UNICODETEXT)
if h == 0 {
return "", err
}
l, _, err := globalLock.Call(h)
if l == 0 {
return "", err
}
text := syscall.UTF16ToString((*[1 << 20]uint16)(unsafe.Pointer(l))[:])
r, _, err := globalUnlock.Call(h)
if r == 0 {
return "", err
}
return text, nil
}
func writeAll(text string) error {
err := waitOpenClipboard()
if err != nil {
return err
}
defer closeClipboard.Call()
r, _, err := emptyClipboard.Call(0)
if r == 0 {
return err
}
data := syscall.StringToUTF16(text)
// "If the hMem parameter identifies a memory object, the object must have
// been allocated using the function with the GMEM_MOVEABLE flag."
h, _, err := globalAlloc.Call(gmemMoveable, uintptr(len(data)*int(unsafe.Sizeof(data[0]))))
if h == 0 {
return err
}
defer func() {
if h != 0 {
globalFree.Call(h)
}
}()
l, _, err := globalLock.Call(h)
if l == 0 {
return err
}
r, _, err = lstrcpy.Call(l, uintptr(unsafe.Pointer(&data[0])))
if r == 0 {
return err
}
r, _, err = globalUnlock.Call(h)
if r == 0 {
if err.(syscall.Errno) != 0 {
return err
}
}
r, _, err = setClipboardData.Call(CF_UNICODETEXT, h)
if r == 0 {
return err
}
h = 0 // suppress deferred cleanup
return nil
}

22
gui_wails03_learn/constants/constants.go

@ -0,0 +1,22 @@
package constants
const (
TrackStatusPending = "pending"
TrackStatusProcessing = "processing"
TrackStatusFailed = "failed"
TrackStatusDownloading = "downloading"
TrackStatusDownladed = "downloaded"
TrakcConverting = "converting"
TrakcConverted = "converted"
TrakcConvertFailed = "failed"
TrakcConvertQueued = "queued"
NgrokStatusRunning = "running"
NgrokStatusError = "error"
NgrokStatusTimeout = "timeout"
NgrokStatusStopped = "killed"
NotificationTypeSuccess = "success"
NotificationTypeWarning = "warning"
NotificationTypeError = "error"
)

12
gui_wails03_learn/constants/errors.go

@ -0,0 +1,12 @@
package constants
const (
TrackWithoutAudioFormat = constError("Track has not audio/webm format available")
CannotDownloadTrack = ""
)
type constError string
func (e constError) Error() string {
return string(e)
}

219
gui_wails03_learn/db/db.go

@ -0,0 +1,219 @@
package db
import (
"encoding/json"
"fmt"
"os"
"log"
"github.com/pkg/errors"
"github.com/xujiajun/nutsdb"
. "ytd/models"
)
const entriesBucketName = "entriesBucket"
const settingBucketName = "settingsBucket"
const offlinePlaylistsBucketName = "offlinePlaylistsBucket"
var db *nutsdb.DB
//InitializeDb initializes db
func InitializeDb() *nutsdb.DB {
var err error
opt := nutsdb.DefaultOptions
fmt.Println(os.Getenv("HOME") + string(os.PathSeparator) + ".ytd")
opt.Dir = os.Getenv("HOME") + string(os.PathSeparator) + ".ytd"
db, err = nutsdb.Open(opt)
if err != nil {
log.Panic(err)
}
return db
}
//CloseDb .
func CloseDb() {
db.Close()
}
func DbGetAllEntries() []GenericEntry {
data := []GenericEntry{}
if err := db.View(
func(tx *nutsdb.Tx) error {
entries, err := tx.GetAll(entriesBucketName)
if err != nil {
return err
}
for _, entry := range entries {
genericEntry := &GenericEntry{}
json.Unmarshal(entry.Value, genericEntry)
data = append(data, *genericEntry)
}
return nil
}); err != nil {
log.Println(err)
} else {
return data
}
return data
}
func DbWriteEntry(Key string, value interface{}) error {
err := db.Update(
func(tx *nutsdb.Tx) error {
keyBytes := []byte(Key)
valueBytes, err := json.Marshal(value)
if err != nil {
return err
}
if err := tx.Put(entriesBucketName, keyBytes, valueBytes, 0); err != nil {
return err
}
return nil
})
return err
}
func DbDeleteEntry(Key string) error {
err := db.Update(
func(tx *nutsdb.Tx) error {
key := []byte(Key)
if err := tx.Delete(entriesBucketName, key); err != nil {
return err
}
return nil
})
return err
}
func DbGetAllOfflinePlaylists() []OfflinePlaylist {
data := []OfflinePlaylist{}
if err := db.View(
func(tx *nutsdb.Tx) error {
entries, err := tx.GetAll(offlinePlaylistsBucketName)
if err != nil {
return err
}
for _, entry := range entries {
playlist := &OfflinePlaylist{}
json.Unmarshal(entry.Value, playlist)
data = append(data, *playlist)
}
return nil
}); err != nil {
log.Println(err)
} else {
return data
}
return data
}
func DbAddOfflinePlaylist(Key string, value interface{}, removeIfExists bool) error {
if removeIfExists {
err := DbRemoveOfflinePlaylist(Key)
if err != nil {
return errors.Wrap(err, "DbAddOfflinePlaylist removeIfExists")
}
}
err := db.Update(
func(tx *nutsdb.Tx) error {
keyBytes := []byte(Key)
valueBytes, err := json.Marshal(value)
if err != nil {
return err
}
if err := tx.Put(offlinePlaylistsBucketName, keyBytes, valueBytes, 0); err != nil {
return err
}
return nil
})
return err
}
func DbRemoveOfflinePlaylist(Key string) error {
err := db.Update(
func(tx *nutsdb.Tx) error {
key := []byte(Key)
if err := tx.Delete(offlinePlaylistsBucketName, key); err != nil {
return err
}
return nil
})
return err
}
//DbWriteSetting .
func DbWriteSetting(Name string, value string) error {
err := db.Update(
func(tx *nutsdb.Tx) error {
keyBytes := []byte(Name)
valueBytes := []byte(value)
if err := tx.Put(settingBucketName, keyBytes, valueBytes, 0); err != nil {
return err
}
return nil
})
return err
}
func DbSaveSettingBoolValue(name string, val bool) error {
var v string
if val {
v = "1"
} else {
v = "0"
}
return DbWriteSetting(name, v)
}
//DbReadSetting .
func DbReadSetting(Name string) (string, error) {
result := ""
key := []byte(Name)
if err := db.View(
func(tx *nutsdb.Tx) error {
bytes, err := tx.Get(settingBucketName, key)
if err != nil {
return err
}
result = string(bytes.Value)
return err
}); err != nil {
return "", err
}
return result, nil
}
func DbReadSettingBoolValue(name string) (bool, error) {
val, err := DbReadSetting(name)
if err != nil {
return false, err
}
if val == "1" {
return true, nil
}
return false, nil
}

17
gui_wails03_learn/frontend/.browserslistrc

@ -0,0 +1,17 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.

16
gui_wails03_learn/frontend/.editorconfig

@ -0,0 +1,16 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false

45
gui_wails03_learn/frontend/.gitignore

@ -0,0 +1,45 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
# Only exists if Bazel was run
/bazel-out
# dependencies
/node_modules
# profiling files
chrome-profiler-events*.json
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db

27
gui_wails03_learn/frontend/README.md

@ -0,0 +1,27 @@
# Frontend
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 12.1.0.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.

119
gui_wails03_learn/frontend/angular.json

@ -0,0 +1,119 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"frontend": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
},
"@schematics/angular:application": {
"strict": true
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "ngx-build-plus:browser",
"options": {
"outputPath": "dist",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css",
"src/styles.scss"
],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "4mb",
"maximumError": "8mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "8kb",
"maximumError": "16kb"
}
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"outputHashing": "all"
},
"development": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.hmr.ts"
}
],
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "ngx-build-plus:dev-server",
"configurations": {
"production": {
"browserTarget": "frontend:build:production"
},
"development": {
"browserTarget": "frontend:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "frontend:build"
}
},
"test": {
"builder": "ngx-build-plus:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"./node_modules/@angular/material/prebuilt-themes/indigo-pink.css",
"src/styles.scss"
],
"scripts": []
}
}
}
}
},
"defaultProject": "frontend"
}

44
gui_wails03_learn/frontend/karma.conf.js

@ -0,0 +1,44 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(__dirname, './coverage/frontend'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
]
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

26596
gui_wails03_learn/frontend/package-lock.json

File diff suppressed because it is too large

55
gui_wails03_learn/frontend/package.json

@ -0,0 +1,55 @@
{
"name": "frontend",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "npx ng build --single-bundle true --output-hashing none --aot --build-optimizer --optimization --allowed-common-js-dependencies",
"build:no:optimizers": "npx ng build --single-bundle true --output-hashing none --aot --build-optimizer=false --optimization=false --allowed-common-js-dependencies",
"watch": "ng build --watch --configuration development",
"serve": "npx --max_old_space_size=4096 ng serve --poll=2000 --host=0.0.0.0 --single-bundle --configuration development",
"test": "ng test"
},
"private": true,
"dependencies": {
"@angular/animations": "~12.1.0",
"@angular/cdk": "^12.1.0",
"@angular/common": "~12.1.0",
"@angular/compiler": "~12.1.0",
"@angular/core": "~12.1.0",
"@angular/flex-layout": "*",
"@angular/forms": "~12.1.0",
"@angular/material": "^12.1.0",
"@angular/platform-browser": "~12.1.0",
"@angular/platform-browser-dynamic": "~12.1.0",
"@angular/router": "~12.1.0",
"@ngx-translate/core": "^13.0.0",
"@ngx-translate/http-loader": "^6.0.0",
"@tinkoff/ng-event-plugins": "^2.2.0",
"@wails/runtime": "^1.3.20",
"@wailsapp/runtime": "^1.1.1",
"await-to-js": "^3.0.0",
"core-js": "^3.15.1",
"lodash": "^4.17.21",
"ng-qrcode": "^5.1.1",
"ngx-build-plus": "^12.0.1",
"rxjs": "~6.6.0",
"tslib": "^2.2.0",
"zone.js": "~0.11.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "~12.1.0",
"@angular/cli": "~12.1.0",
"@angular/compiler-cli": "~12.1.0",
"@types/jasmine": "~3.6.0",
"@types/lodash": "^4.14.172",
"@types/node": "^12.20.19",
"jasmine-core": "~3.7.0",
"karma": "~6.3.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.0.3",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"typescript": "~4.3.2"
}
}

1
gui_wails03_learn/frontend/package.json.md5

@ -0,0 +1 @@
68adfb04b171d5f769382862f9eb5972

28
gui_wails03_learn/frontend/src/app/app-routing.module.ts

@ -0,0 +1,28 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './pages/home/home.component';
import { OfflinePlaylistComponent } from './pages/offline-playlist/offline-playlist.component';
const routes: Routes = [
{
path: '',
pathMatch: 'full',
redirectTo: 'home'
},
{
path: 'home',
component: HomeComponent,
children: [
{
path: 'playlist/:playlist',
component: OfflinePlaylistComponent
}
]
}
];
@NgModule({
imports: [RouterModule.forRoot(routes, {useHash:true})],
exports: [RouterModule]
})
export class AppRoutingModule { }

3
gui_wails03_learn/frontend/src/app/app.component.html

@ -0,0 +1,3 @@
<router-outlet></router-outlet>
<audio-player></audio-player>
<loader-overlay></loader-overlay>

0
gui_wails03_learn/frontend/src/app/app.component.scss

35
gui_wails03_learn/frontend/src/app/app.component.spec.ts

@ -0,0 +1,35 @@
import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
declarations: [
AppComponent
],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'frontend'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('frontend');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('.content span').textContent).toContain('frontend app is running!');
});
});

55
gui_wails03_learn/frontend/src/app/app.component.ts

@ -0,0 +1,55 @@
import { ApplicationRef, ChangeDetectorRef, Component, NgZone } from '@angular/core';
import { MatIconRegistry } from '@angular/material/icon';
import { DomSanitizer } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import * as Wails from '@wails/runtime';
import { RegisterCustomIcons } from './common/custom-icons';
import { LoaderService, LoaderEventBackend } from './services/loader.service';
@Component({
selector: 'app-root,[id="app"]',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
constructor(
private _ngZone: NgZone,
private _cdr: ChangeDetectorRef,
private _appRef: ApplicationRef,
private _loader: LoaderService,
private _matIconRegistry: MatIconRegistry,
private _domSanitizer: DomSanitizer,
private _translateService: TranslateService
) {
RegisterCustomIcons(this._matIconRegistry, this._domSanitizer);
this._translateService.addLangs(['en', 'it', 'pl']);
this._translateService.use(window.APP_STATE.config.Language || 'en');
}
ngOnInit() {
window.addEventListener('focus', () => Wails.Events.Emit("ytd:app:focused"));
window.addEventListener('blur', () => Wails.Events.Emit("ytd:app:blurred"));
document.addEventListener("visibilitychange", (event) => {
Wails.Events.Emit("ytd:app:foreground", { isInForeground: document.visibilityState === 'visible' })
if(document.visibilityState === 'visible') {
// run tick such us I encoutered some issues ex open settings from ytd tray -> settings when windows is not visible
this._appRef.tick();
}
});
Wails.Events.On("ytd:loader:show", (payload: LoaderEventBackend) => {
this._loader.show(payload.label, null , payload.templateName);
});
Wails.Events.On("ytd:loader:hide", () => {
this._loader.hide();
});
Wails.Events.On("ytd:notification", payload => {
console.log('New notification from backend', payload)
})
}
}

124
gui_wails03_learn/frontend/src/app/app.module.ts

@ -0,0 +1,124 @@
import { APP_BASE_HREF, CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClient, HttpClientModule } from '@angular/common/http';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { HomeComponent, OfflinePlaylistComponent } from './pages';
import { FlexLayoutModule } from '@angular/flex-layout';
import { ClipboardModule } from '@angular/cdk/clipboard';
import { MatButtonModule } from '@angular/material/button';
import { MatBadgeModule } from '@angular/material/badge';
import { MatCardModule } from '@angular/material/card';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDialogModule, MAT_DIALOG_DEFAULT_OPTIONS } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatSliderModule} from '@angular/material/slider';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatSnackBarModule, MAT_SNACK_BAR_DEFAULT_OPTIONS } from '@angular/material/snack-bar';
import { MatTabsModule } from '@angular/material/tabs';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { NG_EVENT_PLUGINS } from '@tinkoff/ng-event-plugins';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { QrCodeModule } from 'ng-qrcode';
import {
AudioPlayerComponent,
LoaderOverlayComponent,
SettingsComponent,
UpdaterComponent,
ConfirmationDialogComponent,
AddToPlaylistComponent,
CreatePlaylistComponent,
LanguageSwitcherComponent
} from './components';
import { AutofocusDirective } from './directives';
import { environment } from 'environments/environment';
export function HttpLoaderFactory(http: HttpClient) {
const assetsDir = environment.production ? 'http://localhost:8080/static/frontend/dist/assets/i18n/' : './assets/i18n/'
return new TranslateHttpLoader(http, assetsDir, '.json');
}
@NgModule({
declarations: [
AppComponent,
HomeComponent,
OfflinePlaylistComponent,
AudioPlayerComponent,
SettingsComponent,
UpdaterComponent,
AddToPlaylistComponent,
CreatePlaylistComponent,
LanguageSwitcherComponent,
ConfirmationDialogComponent,
LoaderOverlayComponent,
// directives
AutofocusDirective
],
imports: [
BrowserModule,
CommonModule,
HttpClientModule,
AppRoutingModule,
BrowserAnimationsModule,
FormsModule,
ReactiveFormsModule,
TranslateModule.forRoot({
defaultLanguage: 'en',
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
}),
FlexLayoutModule,
QrCodeModule,
ClipboardModule,
MatBadgeModule,
MatButtonModule,
MatCardModule,
MatCheckboxModule,
MatDialogModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatMenuModule,
MatProgressBarModule,
MatProgressSpinnerModule,
MatSelectModule,
MatSlideToggleModule,
MatSliderModule,
MatSidenavModule,
MatSnackBarModule,
MatTabsModule,
MatToolbarModule,
MatTooltipModule
],
providers: [
{provide: APP_BASE_HREF, useValue : '/' },
NG_EVENT_PLUGINS,
{ provide: MAT_SNACK_BAR_DEFAULT_OPTIONS, useValue: { duration: 5000 } },
{
provide: MAT_DIALOG_DEFAULT_OPTIONS,
useValue: {
backdropClass: 'blurred-backdrop-bg',
hasBackdrop: true,
autoFocus: false
}
},
],
bootstrap: [AppComponent]
})
export class AppModule { }

19
gui_wails03_learn/frontend/src/app/common/custom-icons.ts

@ -0,0 +1,19 @@
import { MatIconRegistry } from "@angular/material/icon";
import { DomSanitizer } from "@angular/platform-browser";
export function RegisterCustomIcons(matIconRegistry: MatIconRegistry, domSanitizer: DomSanitizer) {
matIconRegistry.addSvgIcon(
'github',
domSanitizer.bypassSecurityTrustResourceUrl('http://localhost:8080/static/frontend/dist/assets/icons/github.svg')
)
matIconRegistry.addSvgIcon(
'qrcode_scan',
domSanitizer.bypassSecurityTrustResourceUrl('http://localhost:8080/static/frontend/dist/assets/icons/qrcode-scan.svg')
)
matIconRegistry.addSvgIcon(
'download_failed',
domSanitizer.bypassSecurityTrustResourceUrl('http://localhost:8080/static/frontend/dist/assets/icons/download-failed.svg')
)
}

3
gui_wails03_learn/frontend/src/app/common/fn.ts

@ -0,0 +1,3 @@
export const minMax = (min: number, max: number) => {
return Math.floor(Math.random() * (max - min)) + min;
}

40
gui_wails03_learn/frontend/src/app/components/audio-player/audio-player.component.html

@ -0,0 +1,40 @@
<div class="player" fxLayout="row" fxLayoutGap="12px" [hidden]="!track">
<ng-container *ngIf="track">
<div class="name" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="12px">
<img [src]="trackCover" />
<div class="label" fxLayout="column" fxLayoutGap="3px">
<span class="title">{{ track.name }}</span>
<span class="author">{{ track.author }}</span>
</div>
</div>
<div class="controls" fxFlex fxLayout="column">
<div class="btns" fxLayout="row" fxLayoutAlign="center center" fxLayoutGap="12px">
<mat-icon class="shuffle clickable" title="Shuffle" (click)="shuffle()">shuffle</mat-icon>
<mat-icon class="prew clickable" title="Previous track" (click)="prev()">skip_previous</mat-icon>
<mat-icon class="play clickable" title="Play" [class.disabled]="!isReady()" (click)="play()" *ngIf="!isPlaying">play_arrow</mat-icon>
<mat-icon class="stop clickable" title="Stop" (click)="stop()" *ngIf="isPlaying">stop</mat-icon>
<mat-icon class="next clickable" title="Next track" (click)="next()">skip_next</mat-icon>
<mat-icon class="replay clickable" title="Replay" (click)="replay()">replay</mat-icon>
</div>
<div class="" fxLayout="row" fxLayoutAlign="center center" fxLayoutGap="8px">
<div class="duration">{{ elapsedTime }}</div>
<mat-progress-bar [mode]="isReady() ? 'determinate' : 'indeterminate'" [value]="elapsedTimeProgress" color="warn">
</mat-progress-bar>
<div class="duration">{{ duration * 1000 | date: 'mm:ss' }}</div>
</div>
</div>
<div class="volume" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="4px">
<mat-icon>{{ volume === 0 ? 'volume_off' : 'volume_up' }}</mat-icon>
<mat-slider
[max]="1"
[min]="0"
[step]="0.1"
[tickInterval]="0.1"
[value]="volume"
(input)="changeVolume($event)"
[vertical]="false">
</mat-slider>
</div>
<mat-icon class="close-player clickable" (click)="closePlayer()">close</mat-icon>
</ng-container>
</div>

121
gui_wails03_learn/frontend/src/app/components/audio-player/audio-player.component.scss

@ -0,0 +1,121 @@
body {
&.fullscreen {
audio-player {
.player {
width: 100%;
max-width: calc(100% - 250px - 32px);
left: 250px;
transform: none;
margin: 0 16px;
&[view="playlist"] {
max-width: calc(100% - 250px) !important;
border-radius: unset;
bottom: 0px;
margin: 0;
background: transparent;
}
}
}
}
&:not(.fullscreen) {
audio-player {
.player {
&[view="playlist"] {
width: 100%;
border-radius: unset;
bottom: 0px;
margin: 0;
background: transparent;
}
}
}
}
}
@keyframes player-slide-up {
0% {
height: 0;
opacity: 0;
}
25% {
height: 96px;
opacity: 0.5;
}
100% {
opacity: 1;
}
}
audio-player {
.player {
width: calc(100% - 32px);
max-width: calc(1024px - 32px);
display: flex;
background: rgba(0, 0, 0, 0.85);
position: fixed;
border: 0;
bottom: 10px;
align-items: center;
border-radius: 12px;
left: 50%;
transform: translateX(-50%);
padding: 8px 16px;
box-sizing: border-box;
z-index: 9999;
color: #fff;
transition: all .5s ease-in-out;
&:not([hidden]) {
animation: 0.5s player-slide-up;
height: 96px;
}
.close-player {
margin-right: -10px;
align-self: flex-start;
}
.name {
max-width: 300px;
width: 300px;
img {
height: 70px;
width: 70px;
border-radius: 8px;
object-fit: cover;
}
.label {
font-size: 0.75rem;
.author {
font-weight: 100;
}
}
}
.controls {
.btns {
margin-bottom: 12px;
}
}
.volume {
.mat-slider {
.mat-slider-track-wrapper {
background: #fff;
}
&:not(.mat-slider.sliding) {
.mat-slider-thumb, .mat-slider-thumb-label {
background-color: #fff;
}
}
}
}
}
}

182
gui_wails03_learn/frontend/src/app/components/audio-player/audio-player.component.ts

@ -0,0 +1,182 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
EventEmitter,
HostBinding,
Inject,
Input,
OnInit,
Output,
Renderer2,
ViewEncapsulation,
} from '@angular/core';
import { filter } from 'rxjs/operators'
import { Track } from '@models';
import { AudioPlayerService } from './audio-player.service';
import { SnackbarService } from 'app/services/snackbar.service';
import { MatSliderChange } from '@angular/material/slider';
import { DOCUMENT } from '@angular/common';
@Component({
selector: 'audio-player',
templateUrl: './audio-player.component.html',
styleUrls: ['./audio-player.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
})
export class AudioPlayerComponent implements OnInit {
public track: Track = null;
public audio: HTMLAudioElement = null;
public volume: number = 0.5;
public isPlaying: Boolean = false;
public canPlay: Boolean = false;
public duration: number = null;
public elapsedTime: string = null;
public elapsedTimeProgress: number = null;
public get trackCover(): string {
return this.track.thumbnails[3];
}
@Output() public onPlayAudio: any = new EventEmitter<any>();
constructor(
private _cdr: ChangeDetectorRef,
@Inject(DOCUMENT) private document: Document,
private _snackbar: SnackbarService,
private _audioPlayerService: AudioPlayerService
) {}
ngOnInit(): void {
this._audioPlayerService.onPlaybackTrack.pipe(filter(track => track !== null)).subscribe(track => {
if(this.track && this.track.id === track.id) {
this.play();
return;
}
if(this.audio) {
this.audio.pause();
this.audio = null;
}
this.track = track;
this._play(track);
})
this._audioPlayerService.onStopTrack.pipe(filter(track => track !== null)).subscribe(track => {
this.stop();
})
}
private _play(track: Track): void {
const src = track.isConvertedToMp3 ? `http://localhost:8080/tracks/youtube/${track.id}.mp3` : `http://localhost:8080/tracks/youtube/${track.id}.webm`;
this.audio = new Audio(src);
this.audio.volume = this.volume;
this.audio.ontimeupdate = (e) => {
if(!this.audio) {
return;
}
const s = parseInt((this.audio.currentTime % 60).toString(), 10);
const m = parseInt(((this.audio.currentTime / 60) % 60).toString(), 10);
this.duration = this.audio.duration;
this.elapsedTimeProgress = +(
(+this.audio.currentTime.toFixed(1) / +this.audio.duration.toFixed(1)) *
100
).toFixed(0);
this.elapsedTime = s < 10 ? m + ':0' + s : m + ':' + s;
this._cdr.detectChanges();
};
this.audio.onended = (e) => {
this.isPlaying = false;
this.elapsedTimeProgress = 0;
this._cdr.detectChanges();
};
this.audio.onerror = (e) => {
console.log("Track playback error", e)
this.track = null;
this._snackbar.openError("Cannot playback probably track's file does not exists");
};
this.play();
}
reloadAudio(): void {
this.audio.load();
}
ngDoCheck(): void {
if (this.audio && this.audio.readyState === 4 && !this.canPlay) {
this.canPlay = true;
this._cdr.detectChanges();
}
}
play(): void {
this.document.body.classList.add('player-visible');
this.onPlayAudio.emit();
this.isPlaying = true;
this.audio.play();
this._audioPlayerService.onPlayCmdTrack.next(this.track);
}
stop(): void {
this.isPlaying = false;
this.audio.pause();
this._audioPlayerService.onStopCmdTrack.next(this.track);
this._cdr.detectChanges();
}
changeVolume(event: MatSliderChange): void {
this.volume = event.value;
this.audio.volume = event.value;
}
prev(): void {
this._audioPlayerService.onPrevTrackCmd.next(this.track);
}
next(): void {
this._audioPlayerService.onNextTrackCmd.next(this.track);
}
replay(): void {
this.audio.currentTime = 0;
this.audio.play();
}
shuffle(): void {
this._audioPlayerService.onShuffleTrackCmd.next(this.track);
}
isReady(): Boolean {
return this.audio && this.audio.readyState === 4;
}
closePlayer(): void {
this.audio.pause();
this._audioPlayerService.onStopCmdTrack.next(this.track);
this.audio = null;
this.track = null;
this._audioPlayerService.onClosePlayer.next(true);
this.document.body.classList.remove('player-visible');
}
ngOnDestroy(): void {
this.audio.pause();
this.audio = null;
}
}

63
gui_wails03_learn/frontend/src/app/components/audio-player/audio-player.service.ts

@ -0,0 +1,63 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { Track } from '@models';
@Injectable({providedIn: 'root'})
export class AudioPlayerService {
public action: 'play' | 'stop';
public track: Track = null;
public onPlaybackTrack: BehaviorSubject<Track>;
public onPlayCmdTrack: BehaviorSubject<Track>;
public onStopTrack: BehaviorSubject<Track>;
public onStopCmdTrack: BehaviorSubject<Track>;
public onPrevTrack: BehaviorSubject<Track>;
public onPrevTrackCmd: BehaviorSubject<Track>;
public onNextTrack: BehaviorSubject<Track>;
public onNextTrackCmd: BehaviorSubject<Track>;
public onShuffleTrack: BehaviorSubject<Track>;
public onShuffleTrackCmd: BehaviorSubject<Track>;
public onClosePlayer: Subject<boolean>;
public get trackId(): string {
if(!this.track) {
return null;
}
return this.track.id;
}
constructor() {
this.onPlaybackTrack = new BehaviorSubject(null);
this.onPlayCmdTrack = new BehaviorSubject(null);
this.onStopTrack = new BehaviorSubject(null);
this.onStopCmdTrack = new BehaviorSubject(null);
this.onPrevTrack = new BehaviorSubject(null);
this.onPrevTrackCmd = new BehaviorSubject(null);
this.onNextTrack = new BehaviorSubject(null);
this.onNextTrackCmd = new BehaviorSubject(null);
this.onShuffleTrack = new BehaviorSubject(null);
this.onShuffleTrackCmd = new BehaviorSubject(null);
this.onClosePlayer = new Subject();
this.onPlaybackTrack.subscribe(track => {
this.action = 'play';
this.track = track;
});
this.onStopTrack.subscribe(track => {
this.action = 'stop';
});
}
}

20
gui_wails03_learn/frontend/src/app/components/confirmation-dialog/confirmation-dialog.component.html

@ -0,0 +1,20 @@
<div class="dialog-content-wrapper">
<mat-toolbar>
<mat-toolbar-row fxFlex fxLayout="row" fxLayoutAlign="space-between center">
<span class="title dialog-title">
{{ data.title | translate }}
</span>
<button mat-icon-button (click)="cancel()" >
<mat-icon>close</mat-icon>
</button>
</mat-toolbar-row>
</mat-toolbar>
<div mat-dialog-content>
<p [innerHTML]="data.text | translate"></p>
</div>
<div mat-dialog-actions>
<button mat-button (click)="cancel()">{{ (data.cancelBtnLabel || cancelBtnLabel || 'FORMS.BTNS.CANCEL') | translate }}</button>
<button mat-button (click)="delete()">{{ (data.okBtnLabel || okBtnLabel || 'FORMS.BTNS.YES') | translate }}</button>
</div>
</div>

21
gui_wails03_learn/frontend/src/app/components/confirmation-dialog/confirmation-dialog.component.scss

@ -0,0 +1,21 @@
:host {
[mat-dialog-content] {
padding: 16px 16px 0px;
p {
text-align: center;
}
}
[mat-dialog-actions] {
justify-content: space-between;
button {
margin-left: 16px;
&:first-child {
margin-left: 0;
}
}
}
}

47
gui_wails03_learn/frontend/src/app/components/confirmation-dialog/confirmation-dialog.component.ts

@ -0,0 +1,47 @@
import {
ChangeDetectionStrategy,
Component,
HostListener,
Inject,
Input
} from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
@Component({
selector: 'confirmation-dialog',
templateUrl: './confirmation-dialog.component.html',
styleUrls: ['./confirmation-dialog.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ConfirmationDialogComponent {
@Input()
public cancelBtnLabel: string = null;
@Input()
public okBtnLabel: string = null;
constructor(
@Inject(MAT_DIALOG_DATA) public data: {
text: string,
title: string,
cancelBtnLabel: string;
okBtnLabel: string;
},
public dialogRef: MatDialogRef<ConfirmationDialogComponent>,
) { }
@HostListener('keydown.esc')
public onEsc(): void {
this.cancel();
}
public delete(): void {
this.dialogRef.close(true);
}
public cancel(): void {
this.dialogRef.close(false);
}
}

7
gui_wails03_learn/frontend/src/app/components/index.ts

@ -0,0 +1,7 @@
export * from './audio-player/audio-player.component';
export * from './settings/settings.component';
export * from './updater/updater.component';
export * from './loader/loader.component';
export * from './confirmation-dialog/confirmation-dialog.component';
export * from './language-switcher/language-switcher.component';
export * from './playlist';

9
gui_wails03_learn/frontend/src/app/components/language-switcher/language-switcher.component.html

@ -0,0 +1,9 @@
<mat-icon class="clickable" (click)="triggerSelect()" title="{{ 'LANGUAGE.ICON_DESC' | translate }}">translate</mat-icon>
<mat-form-field class="lang-switch hide">
<mat-label></mat-label>
<mat-select #matSelect [(ngModel)]="model.lang" (selectionChange)="changeLanguage($event)">
<mat-option *ngFor="let lang of langs" [value]="lang">
{{ langTransMap[lang] | translate }}
</mat-option>
</mat-select>
</mat-form-field>

24
gui_wails03_learn/frontend/src/app/components/language-switcher/language-switcher.component.scss

@ -0,0 +1,24 @@
language-switcher {
.mat-icon {
color: white;
}
.mat-form-field {
&.hide {
visibility: hidden;
width: 0;
height: 0;
}
.mat-form-field-underline {
background-color: white;
}
.mat-form-field-label,
.mat-select-value,
.mat-select-arrow {
color: white;
}
}
}

62
gui_wails03_learn/frontend/src/app/components/language-switcher/language-switcher.component.ts

@ -0,0 +1,62 @@
import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef, ViewEncapsulation, ViewChild, ApplicationRef } from '@angular/core';
import { Subscription } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import { MatSelect, MatSelectChange } from '@angular/material/select';
import * as Wails from '@wailsapp/runtime';
import to from 'await-to-js';
import { SnackbarService } from 'app/services/snackbar.service';
@Component({
selector: 'language-switcher',
templateUrl: './language-switcher.component.html',
styleUrls: ['./language-switcher.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None
})
export class LanguageSwitcherComponent {
public langs: string[];
public model: any = {
lang: null
}
public langTransMap: any = {
'en': 'LANGUAGE.EN',
'pl': 'LANGUAGE.PL',
'it': 'LANGUAGE.IT',
}
@ViewChild('matSelect')
private _matSelectRef: MatSelect = null;
constructor(
private _appRef: ApplicationRef,
private _cdr: ChangeDetectorRef,
private _translateService: TranslateService,
private _snackbar: SnackbarService
) {
this.langs = this._translateService.langs;
this.model.lang = this._translateService.currentLang;
}
triggerSelect(): void {
this._matSelectRef.open();
}
public changeLanguage(change: MatSelectChange): void {
const lang = change.value;
this._translateService.getTranslation(lang).subscribe(async (translations) => {
this._translateService.use(lang);
const [error, saved] = await to(window.backend.main.AppState.SaveSettingValue('Language', lang));
if(error) {
this._snackbar.openError(this._translateService.instant("LANGUAGE.SAVE_ERROR"));
return;
}
const [errorReload, _] = await to(window.backend.main.AppState.ReloadNewLanguage());
this._appRef.tick();
})
}
}

33
gui_wails03_learn/frontend/src/app/components/loader/loader.component.html

@ -0,0 +1,33 @@
<div class="loader-overlay" [hidden]="isHidden()">
<div class="content-template">
<ng-container
*ngTemplateOutlet="loadingState.templateRef ? loadingState.templateRef : templatesMap[loadingState.templateName]; context: {label: loadingState.label, data: loadingState.data}"
></ng-container>
</div>
</div>
<ng-template #defaultLoaderTpl let-label="label">
<div class="sk-cube-grid">
<div class="sk-cube sk-cube1"></div>
<div class="sk-cube sk-cube2"></div>
<div class="sk-cube sk-cube3"></div>
<div class="sk-cube sk-cube4"></div>
<div class="sk-cube sk-cube5"></div>
<div class="sk-cube sk-cube6"></div>
<div class="sk-cube sk-cube7"></div>
<div class="sk-cube sk-cube8"></div>
<div class="sk-cube sk-cube9"></div>
</div>
<div class="label" *ngIf="label">{{ label | translate }}</div>
</ng-template>
<ng-template #cardLoaderTpl let-label="label">
<mat-card class="card-tpl">
<mat-card-header *ngIf="label">
<mat-card-title>{{ label | translate }}</mat-card-title>
</mat-card-header>
<mat-card-content>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</mat-card-content>
</mat-card>
</ng-template>

107
gui_wails03_learn/frontend/src/app/components/loader/loader.component.scss

@ -0,0 +1,107 @@
.loader-overlay {
&::before {
content: "";
display: block;
width: 100%;
height: 100%;
background-color: #2c3e50;
opacity: 0.6;
z-index: 9998;
position: fixed;
top: 0;
left: 0;
}
.label {
text-align: center;
color: #fff;
font-size: 18px;
z-index: 9999;
padding-top: 110px;
}
.content-template {
position: fixed;
text-align: center;
top: 50%;
transform: translate(-50%, -50%);
left: 50%;
color: #fff;
z-index: 9999;
.card-tpl {
max-width: 30vw;
width: fit-content;
min-width: 100px;
}
}
}
.sk-cube-grid {
width: 75px;
height: 75px;
position: fixed;
top: 0;
left: 0;
overflow: visible;
margin: auto;
bottom: 0;
right: 0;
z-index: 9999;
}
.sk-cube-grid .sk-cube {
width: 33%;
height: 33%;
background-color: #fff ;
float: left;
-webkit-animation: sk-cubeGridScaleDelay 1.3s infinite ease-in-out;
animation: sk-cubeGridScaleDelay 1.3s infinite ease-in-out;
}
.sk-cube-grid .sk-cube1 {
-webkit-animation-delay: 0.2s;
animation-delay: 0.2s; }
.sk-cube-grid .sk-cube2 {
-webkit-animation-delay: 0.3s;
animation-delay: 0.3s; }
.sk-cube-grid .sk-cube3 {
-webkit-animation-delay: 0.4s;
animation-delay: 0.4s; }
.sk-cube-grid .sk-cube4 {
-webkit-animation-delay: 0.1s;
animation-delay: 0.1s; }
.sk-cube-grid .sk-cube5 {
-webkit-animation-delay: 0.2s;
animation-delay: 0.2s; }
.sk-cube-grid .sk-cube6 {
-webkit-animation-delay: 0.3s;
animation-delay: 0.3s; }
.sk-cube-grid .sk-cube7 {
-webkit-animation-delay: 0s;
animation-delay: 0s; }
.sk-cube-grid .sk-cube8 {
-webkit-animation-delay: 0.1s;
animation-delay: 0.1s; }
.sk-cube-grid .sk-cube9 {
-webkit-animation-delay: 0.2s;
animation-delay: 0.2s; }
@-webkit-keyframes sk-cubeGridScaleDelay {
0%, 70%, 100% {
-webkit-transform: scale3D(1, 1, 1);
transform: scale3D(1, 1, 1);
} 35% {
-webkit-transform: scale3D(0, 0, 1);
transform: scale3D(0, 0, 1);
}
}
@keyframes sk-cubeGridScaleDelay {
0%, 70%, 100% {
-webkit-transform: scale3D(1, 1, 1);
transform: scale3D(1, 1, 1);
} 35% {
-webkit-transform: scale3D(0, 0, 1);
transform: scale3D(0, 0, 1);
}
}

62
gui_wails03_learn/frontend/src/app/components/loader/loader.component.ts

@ -0,0 +1,62 @@
import { Component, OnInit, OnDestroy, TemplateRef, ChangeDetectionStrategy, ChangeDetectorRef, QueryList, ViewChildren, ViewChild, AfterViewInit, Inject } from '@angular/core';
import { Subscription } from 'rxjs';
import { LoaderService, LoaderState } from 'app/services/loader.service';
import { DOCUMENT } from '@angular/common';
@Component({
selector: 'loader-overlay',
templateUrl: './loader.component.html',
styleUrls: ['./loader.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class LoaderOverlayComponent implements OnInit, AfterViewInit, OnDestroy {
public loadingState: LoaderState;
public defaultTpl: "default" | "card" = 'card';
public templatesMap: {[key: string]: TemplateRef<any>} = {};
private subscription: Subscription;
@ViewChild('defaultLoaderTpl')
private _defaultLoaderTpl: TemplateRef<any>;
@ViewChild('cardLoaderTpl')
private _cardLoaderTpl: TemplateRef<any>;
constructor(
private _cdr: ChangeDetectorRef,
private _loaderService: LoaderService,
@Inject(DOCUMENT) private _document: Document
) { }
ngOnInit(): void {
this.subscription = this._loaderService.loaderState$.subscribe(
state => {
this.loadingState = state;
if(state.isLoading) {
this._document.body.classList.add('loading');
} else {
this._document.body.classList.remove('loading');
}
this._cdr.detectChanges();
}
);
}
ngAfterViewInit(): void {
this.templatesMap = {
'default': this._defaultLoaderTpl,
'card': this._cardLoaderTpl
}
}
isHidden(): boolean {
return !this.loadingState.isLoading || !this.loadingState.show;
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
}

39
gui_wails03_learn/frontend/src/app/components/playlist/add-to-playlist.component.html

@ -0,0 +1,39 @@
<div class="dialog-content-wrapper">
<mat-toolbar>
<mat-toolbar-row fxFlex fxLayout="row" fxLayoutAlign="space-between center">
<span class="title dialog-title">
{{ 'PLAYLISTS.ADD_TRACK' | translate }}
</span>
<button mat-icon-button (click)="close()" >
<mat-icon>close</mat-icon>
</button>
</mat-toolbar-row>
</mat-toolbar>
<div mat-dialog-content>
<div class="playlists" fxLayout="column" fxLayoutGap="12px" *ngFor="let p of data.playlists">
<div class="playlist">
<mat-checkbox
[checked]="isSelectedPlaylist(p.uuid)"
[disabled]="alreadyInPlaylist[p.uuid]"
(change)="add($event, p)"
matTooltip="{{ 'PLAYLISTS.ALREADY_IN_PLAYLIST' | translate }}"
matTooltipPosition="right"
[matTooltipDisabled]="!alreadyInPlaylist[p.uuid]">
{{ p.name }}
</mat-checkbox>
</div>
</div>
<div *ngIf="!data.playlists.length">
{{ 'PLAYLISTS.NO_PLAYLISTS' | translate }}
</div>
</div>
<div mat-dialog-actions>
<button mat-flat-button color="primary" (click)="createNew()">
{{ 'PLAYLISTS.CREATE' | translate }}
</button>
<button mat-flat-button color="primary" (click)="apply()">
{{ 'PLAYLISTS.ADD_BTN' | translate }}
</button>
</div>
</div>

10
gui_wails03_learn/frontend/src/app/components/playlist/add-to-playlist.component.scss

@ -0,0 +1,10 @@
.add-to-playlist-dialog {
.mat-dialog-container {
background: url("http://localhost:8080/static/frontend/dist/assets/bg.png");
background-repeat: no-repeat;
.mat-toolbar {
background: transparent;
}
}
}

65
gui_wails03_learn/frontend/src/app/components/playlist/add-to-playlist.component.ts

@ -0,0 +1,65 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
Inject,
OnInit,
ViewEncapsulation,
} from '@angular/core';
import { MatCheckboxChange } from '@angular/material/checkbox';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { Entry } from '@models';
import { OfflinePlaylist } from 'app/models/offline-playlist';
@Component({
selector: 'add-to-playlist',
templateUrl: './add-to-playlist.component.html',
styleUrls: ['./add-to-playlist.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
})
export class AddToPlaylistComponent implements OnInit {
public selectedPlaylists: string[] = [];
public alreadyInPlaylist: any = {};
constructor(
private _cdr: ChangeDetectorRef,
private _dialogRef: MatDialogRef<AddToPlaylistComponent>,
@Inject(MAT_DIALOG_DATA) public data: { entry: Entry, playlists: OfflinePlaylist[], foundInPlaylists: OfflinePlaylist[] }
) {
}
ngOnInit(): void {
this.selectedPlaylists = this.data.foundInPlaylists.map(p => p.uuid);
this.data.foundInPlaylists.forEach(p => this.alreadyInPlaylist[p.uuid] = true);
this._cdr.detectChanges();
}
isSelectedPlaylist(uuid: string): boolean {
return this.selectedPlaylists.findIndex(pUUID => pUUID === uuid) > -1;
}
add(change: MatCheckboxChange, playlist: OfflinePlaylist): void {
if(change.checked) {
this.selectedPlaylists.push(playlist.uuid);
return;
}
const idx = this.selectedPlaylists.indexOf(playlist.uuid)
this.selectedPlaylists.splice(idx, 1);
}
createNew(): void {
this._dialogRef.close({ action: 'createNew' });
}
apply(): void {
const uuids = Object.keys(this.alreadyInPlaylist);
const diff = this.selectedPlaylists.filter(uuid => uuids.indexOf(uuid) === -1);
this._dialogRef.close({ selectedPlaylists: diff });
}
close(): void {
this._dialogRef.close();
}
}

26
gui_wails03_learn/frontend/src/app/components/playlist/create.component.html

@ -0,0 +1,26 @@
<div class="dialog-content-wrapper">
<mat-toolbar>
<mat-toolbar-row fxFlex fxLayout="row" fxLayoutAlign="space-between center">
<span class="title dialog-title">
{{ 'PLAYLISTS.CREATE' | translate }}
</span>
<button mat-icon-button (click)="close()" >
<mat-icon>close</mat-icon>
</button>
</mat-toolbar-row>
</mat-toolbar>
<div mat-dialog-content>
<mat-form-field appearance="fill">
<mat-label>{{ 'PLAYLISTS.NAME' | translate }}</mat-label>
<input matInput [(ngModel)]="model.name" (keyup.enter)="create()" required [autofocus]>
<mat-error>{{ 'FORMS.VALIDATION.EMPTY' | translate }}</mat-error>
<mat-hint>{{ 'PLAYLISTS.PRESS_ENTER_HINT' | translate }}</mat-hint>
</mat-form-field>
</div>
<div mat-dialog-actions>
<button mat-flat-button color="primary" (click)="create()" [disabled]="!model.name">
{{ 'FORMS.BTNS.CREATE' | translate }}
</button>
</div>
</div>

10
gui_wails03_learn/frontend/src/app/components/playlist/create.component.scss

@ -0,0 +1,10 @@
create-playlist {
[mat-dialog-content] {
padding: 16px 16px 0px !important;
margin: 0;
.mat-form-field {
width: 100%;
}
}
}

47
gui_wails03_learn/frontend/src/app/components/playlist/create.component.ts

@ -0,0 +1,47 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
Inject,
OnInit,
ViewEncapsulation,
} from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { Entry, UpdateRelease } from '@models';
import { OfflinePlaylist } from 'app/models/offline-playlist';
@Component({
selector: 'create-playlist',
templateUrl: './create.component.html',
styleUrls: ['./create.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
})
export class CreatePlaylistComponent implements OnInit {
public model: any = {
name: null
}
constructor(
private _cdr: ChangeDetectorRef,
private _dialogRef: MatDialogRef<CreatePlaylistComponent>,
@Inject(MAT_DIALOG_DATA) public data: { entry: Entry, playlists: OfflinePlaylist[] }
) {
}
ngOnInit(): void {
}
create(): void {
if(!this.model.name) {
return;
}
this._dialogRef.close({ playlist: this.model });
}
close(): void {
this._dialogRef.close();
}
}

2
gui_wails03_learn/frontend/src/app/components/playlist/index.ts

@ -0,0 +1,2 @@
export * from './add-to-playlist.component';
export * from './create.component';

173
gui_wails03_learn/frontend/src/app/components/settings/settings.component.html

@ -0,0 +1,173 @@
<div class="dialog-content-wrapper">
<mat-toolbar>
<mat-toolbar-row fxFlex fxLayout="row" fxLayoutAlign="space-between center">
<span class="title dialog-title">
{{ 'SETTINGS.TITLE' | translate }}
</span>
<button mat-icon-button (click)="close()" >
<mat-icon>close</mat-icon>
</button>
</mat-toolbar-row>
</mat-toolbar>
<div mat-dialog-content>
<mat-tab-group>
<mat-tab label="general">
<ng-template mat-tab-label>
<mat-icon>tune</mat-icon>
{{ 'SETTINGS.TABS.GENERAL' | translate }}
</ng-template>
<div fxLayout="column" fxLayoutGap="12px">
<mat-form-field appearance="fill">
<mat-label>{{ 'SETTINGS.SAVE_DIRECTORY' | translate }}</mat-label>
<input matInput readonly [(ngModel)]="model.BaseSaveDir" required>
<button mat-icon-button matSuffix (click)="changeBaseSaveDir()">
<mat-icon>edit</mat-icon>
</button>
<mat-hint>{{ 'SETTINGS.SAVE_DIRECTORY_HINT' | translate }}</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>{{ 'SETTINGS.MAX_SIM_DOWNLOADS' | translate }}</mat-label>
<input matInput type="number" min="0" max="10" [(ngModel)]="model.MaxParrallelDownloads">
<mat-hint>{{ 'SETTINGS.MAX_SIM_DOWNLOADS_HINT' | translate }}</mat-hint>
</mat-form-field>
<mat-slide-toggle [(ngModel)]="model.ClipboardWatch">
{{ 'SETTINGS.CLIBOARD_WATCH' | translate }}
</mat-slide-toggle>
<mat-slide-toggle [(ngModel)]="model.DownloadOnCopy" [disabled]="!model.ClipboardWatch">
{{ 'SETTINGS.DOWNLOAD_ON_COPY' | translate }}
</mat-slide-toggle>
<mat-slide-toggle [(ngModel)]="model.ConcurrentDownloads">
{{ 'SETTINGS.CONCURRENT_DOWNLOADS' | translate }}
</mat-slide-toggle>
<mat-slide-toggle [(ngModel)]="model.ConcurrentPlaylistDownloads">
{{ 'SETTINGS.CONCURRENT_PLAYLIST_DOWNLOADS' | translate }}
</mat-slide-toggle>
<div class="toggle-wrapper" fxLayout="column" fxLayoutGap="5px">
<mat-slide-toggle [(ngModel)]="model.ConvertToMp3" [disabled]="!isFfmpegAvailable">
{{ 'SETTINGS.CONVERT_TO_MP3' | translate }}
</mat-slide-toggle>
<div class="warning" *ngIf="!isFfmpegAvailable">
{{ 'SETTINGS.MISSING_FFMPEG' | translate }}
</div>
</div>
<div class="toggle-wrapper" fxLayout="column" fxLayoutGap="5px">
<mat-slide-toggle [(ngModel)]="model.CleanWebmFiles" [disabled]="!isFfmpegAvailable || !model.ConvertToMp3">
{{ 'SETTINGS.REMOVE_WEBM_AFTER_CONVERTING' | translate }}
</mat-slide-toggle>
<div class="warning" *ngIf="!model.ConvertToMp3">
{{ 'SETTINGS.MUST_ENABLE_CONVERT_TO_MP3' | translate }}
</div>
</div>
<div class="toggle-wrapper" fxLayout="column" fxLayoutGap="5px" hidden>
<mat-slide-toggle [(ngModel)]="model.Telegram.Share" [disabled]="!isFfmpegAvailable || !model.ConvertToMp3">
{{ 'SETTINGS.TELEGRAM_SHARE' | translate }}
</mat-slide-toggle>
<div class="warning" *ngIf="!model.ConvertToMp3">
{{ 'SETTINGS.MUST_ENABLE_CONVERT_TO_MP3' | translate }}
</div>
<mat-form-field appearance="fill" *ngIf="model.Telegram.Share">
<mat-label>{{ 'SETTINGS.TELEGRAM_USERNAME' | translate }}</mat-label>
<input matInput [(ngModel)]="model.Telegram.Username" required>
<span matPrefix class="at">@</span>
<mat-hint >
You have to set <span class="open-link" title="Click for more details" (click)="openUrl('https://telegram.org/faq?setln=en#q-what-are-usernames-how-do-i-get-one')">username</span> into telegram's settings</mat-hint>
<mat-error>{{ 'SETTINGS.TELEGRAM_USERNAMNE_EMPTY' | translate }}</mat-error>
</mat-form-field>
</div>
</div>
</mat-tab>
<mat-tab label="public_server">
<ng-template mat-tab-label>
<mat-icon>public</mat-icon>
{{ 'SETTINGS.TABS.PUBLIC_SERVER.LABEL' | translate }} ({{ (data.isNgrokRunning && model.PublicServer.Enabled ? 'SETTINGS.TABS.PUBLIC_SERVER.ENABLED' : 'SETTINGS.TABS.PUBLIC_SERVER.DISABLED') | translate }})
</ng-template>
<ng-template matTabContent>
<div class="public-server" fxLayout="column" fxLayoutAlign="center center" fxLayoutGap="12px">
<mat-slide-toggle [(ngModel)]="model.PublicServer.Enabled" (ngModelChange)="reRenderQrcode()" [disabled]="!isNgrokAvailable">
{{ 'SETTINGS.TABS.PUBLIC_SERVER.DESC' | translate }}
</mat-slide-toggle>
<div class="box warning" *ngIf="!isNgrokAvailable">
{{ 'SETTINGS.TABS.PUBLIC_SERVER.NGROK.NOT_INSTALLED' | translate }}
<button mat-flat-button color="secondary" (click)="openUrl('https://ngrok.com/download')">
{{ 'SETTINGS.TABS.PUBLIC_SERVER.NGROK.INSTALL_BTN' | translate }}
</button>
</div>
<div fxLayout="row" fxLayoutAlign="start start" fxLayoutGap="12px" [style.width.%]="100" *ngIf="model.PublicServer.Enabled">
<div class="qrcode">
<qr-code [value]="publicServerQrcode" size="300" errorCorrectionLevel="M"></qr-code>
</div>
<div class="form" fxLayout="column" fxLayoutAlign="start start" fxLayoutGap="16px">
<mat-form-field appearance="fill" [style.width.%]="100">
<mat-label>{{ 'SETTINGS.TABS.PUBLIC_SERVER.NGROK.AUTH_TOKEN' | translate }}</mat-label>
<input matInput [(ngModel)]="model.PublicServer.Ngrok.Authtoken" required>
<mat-error>{{ 'FORMS.VALIDATION.EMPTY' | translate }}</mat-error>
<mat-hint>{{ 'SETTINGS.TABS.PUBLIC_SERVER.NGROK.AUTH_TOKEN_HINT' | translate }}</mat-hint>
</mat-form-field>
<div class="auth" fxLayout="column" fxLayoutAlign="start start" fxLayoutGap="6px">
<mat-slide-toggle [(ngModel)]="model.PublicServer.Ngrok.Auth.Enabled" (ngModelChange)="reRenderQrcode()">
{{ (model.PublicServer.Ngrok.Auth.Enabled ? 'SETTINGS.TABS.PUBLIC_SERVER.NGROK.AUTH_DESC_ENABLED' : 'SETTINGS.TABS.PUBLIC_SERVER.NGROK.AUTH_DESC_DISABLED') | translate }}
</mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="center center" fxLayoutGap="12px">
<mat-form-field appearance="fill">
<mat-label>{{ 'SETTINGS.TABS.PUBLIC_SERVER.NGROK.USERNAME' | translate }}</mat-label>
<input matInput [(ngModel)]="model.PublicServer.Ngrok.Auth.Username" (ngModelChange)="reRenderQrcode()" [required]="model.PublicServer.Ngrok.Auth.Enabled" [disabled]="!model.PublicServer.Ngrok.Auth.Enabled">
<mat-error>{{ 'FORMS.VALIDATION.EMPTY' | translate }}</mat-error>
<mat-hint>{{ 'SETTINGS.TABS.PUBLIC_SERVER.NGROK.USERNAME_HINT' | translate }}</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>{{ 'SETTINGS.TABS.PUBLIC_SERVER.NGROK.PASSWORD' | translate }}</mat-label>
<input matInput [(ngModel)]="model.PublicServer.Ngrok.Auth.Password" (ngModelChange)="reRenderQrcode()" [required]="model.PublicServer.Ngrok.Auth.Enabled" [disabled]="!model.PublicServer.Ngrok.Auth.Enabled">
<mat-error>{{ 'FORMS.VALIDATION.EMPTY' | translate }}</mat-error>
<mat-hint>{{ 'SETTINGS.TABS.PUBLIC_SERVER.NGROK.PASSWORD_HINT' | translate }}</mat-hint>
</mat-form-field>
</div>
</div>
<div class="api-key" fxLayout="column" fxLayoutAlign="start start" fxLayoutGap="6px">
<mat-slide-toggle [(ngModel)]="model.PublicServer.VerifyAppKey" (ngModelChange)="reRenderQrcode()">
{{ (model.PublicServer.VerifyAppKey ? 'SETTINGS.TABS.PUBLIC_SERVER.NGROK.APIKEY_DESC_ENABLED' : 'SETTINGS.TABS.PUBLIC_SERVER.NGROK.APIKEY_DESC_DISABLED') | translate }}
</mat-slide-toggle>
<mat-form-field appearance="fill" [style.width.%]="100">
<mat-label>{{ 'SETTINGS.TABS.PUBLIC_SERVER.NGROK.APP_KEY' | translate }}</mat-label>
<input matInput [(ngModel)]="model.PublicServer.AppKey" (ngModelChange)="reRenderQrcode()" [disabled]="!model.PublicServer.VerifyAppKey">
<mat-hint>{{ 'SETTINGS.TABS.PUBLIC_SERVER.NGROK.APP_KEY_HINT' | translate }}</mat-hint>
</mat-form-field>
</div>
</div>
</div>
<div class="mobile-app box info" *ngIf="false && isNgrokAvailable && model.PublicServer.Enabled">
<span>{{ 'SETTINGS.TABS.PUBLIC_SERVER.NGROK.MOBILE_APP' | translate }}</span>
<button mat-flat-button color="secondary" (click)="openUrl('https://github.com/marcio199226/ytd-mobile/releases')">
{{ 'SETTINGS.TABS.PUBLIC_SERVER.NGROK.MOBILE_APP_BTN' | translate }}
</button>
</div>
<div class="share-url box info" *ngIf="isNgrokAvailable && model.PublicServer.Enabled">
<span>{{ 'SETTINGS.TABS.PUBLIC_SERVER.NGROK.SHARE_URL' | translate }}</span>
<button mat-icon-button [title]="'SETTINGS.TABS.PUBLIC_SERVER.NGROK.COPY_URL' | translate" (click)="copyUrlToClipboard()">
<mat-icon>content_copy</mat-icon>
</button>
</div>
</div>
</ng-template>
</mat-tab>
</mat-tab-group>
</div>
<div mat-dialog-actions>
<button mat-flat-button color="primary" (click)="save()">
{{ 'SETTINGS.SAVE' | translate }}
</button>
</div>
</div>

88
gui_wails03_learn/frontend/src/app/components/settings/settings.component.scss

@ -0,0 +1,88 @@
settings {
[mat-dialog-content] {
padding: 0px 16px;
margin: 0;
}
[mat-dialog-actions] {
justify-content: flex-end;
margin-bottom: 0;
padding: 8px 16px;
}
}
.settings-dialog {
.mat-dialog-container {
background: url("http://localhost:8080/static/frontend/dist/assets/bg.png");
background-repeat: no-repeat;
.mat-toolbar {
background: transparent;
}
.mat-tab-group {
.mat-tab-body {
margin-top: 10px;
max-height: 450px;
}
.mat-tab-label {
color: white;
opacity: 1;
flex: 1;
.mat-icon {
margin-right: 4px;
}
}
}
.open-link {
font-weight: bold;
text-decoration: underline;
cursor: pointer;
}
.at {
font-weight: bold;
padding-right: 8px;
}
.toggle-wrapper {
margin-left: 46px;
.mat-slide-toggle {
margin-left: -46px;
}
.warning {
font-weight: bold;
}
}
// public server settings tabs
.public-server {
.auth {
.mat-form-field {
.mat-form-field-infix {
width: auto;
}
}
}
.api-key {
width: 100%;
}
.mobile-app, .share-url {
margin-top: 6px;
width: 100%;
box-sizing: border-box;
display: flex;
justify-content: space-between;
align-items: center;
}
}
}
}

136
gui_wails03_learn/frontend/src/app/components/settings/settings.component.ts

@ -0,0 +1,136 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
Inject,
OnInit,
ViewChild,
ViewEncapsulation,
} from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import to from 'await-to-js';
import { AppConfig, getQrcodeData, NgrokState } from '@models';
import { MatTabGroup } from '@angular/material/tabs';
import { Clipboard } from '@angular/cdk/clipboard';
import { SnackbarService } from 'app/services/snackbar.service';
@Component({
selector: 'settings',
templateUrl: './settings.component.html',
styleUrls: ['./settings.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
})
export class SettingsComponent implements OnInit {
public model: AppConfig = {
BaseSaveDir: null,
ClipboardWatch: null,
ConcurrentDownloads: null,
ConcurrentPlaylistDownloads: null,
DownloadOnCopy: null,
MaxParrallelDownloads: null,
ConvertToMp3: null,
CleanWebmFiles: null,
RunInBackgroundOnClose: null,
CheckForUpdates: null,
StartAtLogin: null,
Language: null,
Telegram: {
Share: null,
Username: null
},
PublicServer: {
Enabled: false,
Ngrok: {
Authtoken: null,
Auth: {
Enabled: false,
Username: null,
Password: null,
},
},
VerifyAppKey: null,
AppKey: null,
}
};
public isFfmpegAvailable: boolean = false;
public isNgrokAvailable: boolean = false;
public publicServerQrcode: string = "{}";
@ViewChild(MatTabGroup)
private _matTabs: MatTabGroup;
constructor(
private _cdr: ChangeDetectorRef,
private _dialogRef: MatDialogRef<SettingsComponent>,
private _clipboard: Clipboard,
private _snackbar: SnackbarService,
@Inject(MAT_DIALOG_DATA) public data: { tab?: string, config: AppConfig, isNgrokRunning: boolean, ngrok: NgrokState },
) {
}
async ngOnInit(): Promise<void> {
this.model = { ...this.data.config };
const [errFfmpeg, ffmpegPath] = await to(window.backend.main.AppState.IsFFmpegInstalled());
if(ffmpegPath) {
this.isFfmpegAvailable = true;
this._cdr.detectChanges();
}
const [errNgrok, ngrokPath] = await to(window.backend.main.NgrokService.IsNgrokInstalled());
if(ngrokPath) {
this.isNgrokAvailable = true;
this._cdr.detectChanges();
}
if(this.model.PublicServer.Enabled) {
this.reRenderQrcode();
}
}
ngAfterViewInit(): void {
if(this.data.tab) {
const tab = this._matTabs._tabs.find(tab => tab.textLabel === this.data.tab);
this._matTabs.selectedIndex = tab.position;
this._cdr.detectChanges();
}
}
async changeBaseSaveDir(): Promise<void> {
const [err, path] = await to(window.backend.main.AppState.SelectDirectory());
console.log(err, path)
}
async openUrl(url: string): Promise<void> {
await window.backend.main.AppState.OpenUrl(url);
}
save(): void {
this._dialogRef.close({ config: this.model });
}
close(): void {
this._dialogRef.close();
}
reRenderQrcode(): void {
this.publicServerQrcode = this._getQrCodeData();
this._cdr.detectChanges();
}
copyUrlToClipboard(): void {
this._clipboard.copy(this.publicServerQrcode);
this._snackbar.openSuccess("SETTINGS.TABS.PUBLIC_SERVER.NGROK.URL_COPIED");
}
private _getQrCodeData(): string {
const pwa = window.APP_STATE.pwaUrl;
if(this.data.isNgrokRunning) {
return getQrcodeData(this.data.ngrok.url);
}
return "{}";
}
}

33
gui_wails03_learn/frontend/src/app/components/updater/updater.component.html

@ -0,0 +1,33 @@
<div class="dialog-content-wrapper">
<mat-toolbar>
<mat-toolbar-row fxFlex fxLayout="row" fxLayoutAlign="space-between center">
<span class="title dialog-title">
{{ 'UPDATER.TITLE' | translate }}
</span>
<button mat-icon-button (click)="close()" >
<mat-icon>close</mat-icon>
</button>
</mat-toolbar-row>
</mat-toolbar>
<div mat-dialog-content fxLayout="column" fxLayoutGap="12px">
<div class="desc" innerHTML="{{ 'UPDATER.DESC' | translate: { version: data.release.Version, oldVersion: data.oldVersion } }}"></div>
<ul class="assets" *ngFor="let asset of data.release.Assets">
<li (click)="updateManually(asset.Url)" title="{{ 'UPDATER.DOWNLOAD_MANUALLY_TITLE' | translate }}">
<span [innerHTML]="'UPDATER.DOWNLOAD_LINK' | translate: { name: asset.Name }"></span>
</li>
</ul>
<pre>
{{ data.release.Changelog }}
</pre>
</div>
<div mat-dialog-actions>
<!-- <button mat-flat-button color="primary" (click)="updateAndRestart()">{{ 'UPDATER.UPDATE_AND_RESTART_BTN' | translate }}</button> -->
<button mat-flat-button color="primary" (click)="updateAndReplace()">
{{ 'UPDATER.UPDATE_BTN' | translate }}
</button>
<button mat-flat-button color="primary" (click)="close()">
{{ 'UPDATER.LATER_BTN' | translate }}
</button>
</div>
</div>

33
gui_wails03_learn/frontend/src/app/components/updater/updater.component.scss

@ -0,0 +1,33 @@
updater {
[mat-dialog-content] {
padding: 0px 16px;
margin: 0;
}
[mat-dialog-actions] {
justify-content: flex-end;
margin-bottom: 0;
padding: 8px 16px;
}
}
.updater-dialog {
.mat-dialog-container {
background: url("http://localhost:8080/static/frontend/dist/assets/bg.png");
background-repeat: no-repeat;
.mat-toolbar {
background: transparent;
}
.open-link {
font-weight: bold;
text-decoration: underline;
cursor: pointer;
}
pre {
white-space: pre-line;
}
}
}

47
gui_wails03_learn/frontend/src/app/components/updater/updater.component.ts

@ -0,0 +1,47 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
Inject,
OnInit,
ViewEncapsulation,
} from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { UpdateRelease } from '@models';
@Component({
selector: 'updater',
templateUrl: './updater.component.html',
styleUrls: ['./updater.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
})
export class UpdaterComponent implements OnInit {
constructor(
private _cdr: ChangeDetectorRef,
private _dialogRef: MatDialogRef<UpdaterComponent>,
@Inject(MAT_DIALOG_DATA) public data: { release: UpdateRelease, oldVersion: string }
) {
}
ngOnInit(): void {
}
updateAndRestart(): void {
this._dialogRef.close({ action: 'UpdateAndRestart' });
}
updateAndReplace(): void {
this._dialogRef.close({ action: 'UpdateAndReplace' });
}
async updateManually(url: string): Promise<void> {
await window.backend.main.AppState.OpenUrl(url);
}
close(): void {
this._dialogRef.close();
}
}

14
gui_wails03_learn/frontend/src/app/directives/autofocus.directive.ts

@ -0,0 +1,14 @@
import { Directive, OnInit, ElementRef } from '@angular/core';
@Directive({
selector: '[autofocus]'
})
export class AutofocusDirective implements OnInit {
constructor(private elementRef: ElementRef) { };
ngOnInit(): void {
this.elementRef.nativeElement.focus();
}
}

1
gui_wails03_learn/frontend/src/app/directives/index.ts

@ -0,0 +1 @@
export * from './autofocus.directive';

82
gui_wails03_learn/frontend/src/app/models/app-state.ts

@ -0,0 +1,82 @@
import { Entry } from './entry';
import { OfflinePlaylist } from './offline-playlist';
export interface AppConfig {
BaseSaveDir: string;
ClipboardWatch: boolean;
ConcurrentDownloads: boolean;
ConcurrentPlaylistDownloads: boolean;
DownloadOnCopy: boolean;
MaxParrallelDownloads: number;
ConvertToMp3: boolean;
CleanWebmFiles: boolean;
RunInBackgroundOnClose: boolean;
CheckForUpdates: boolean;
StartAtLogin: boolean;
Language: string;
Telegram: {
Share: boolean;
Username: string;
},
PublicServer: {
Enabled: boolean;
Ngrok: {
Authtoken: string;
Auth: {
Enabled: boolean;
Username: string;
Password: string;
},
},
VerifyAppKey: boolean;
AppKey: string;
}
}
export interface AppStats {
DownloadingCount: number;
}
export interface AppState {
entries: Entry[];
offlinePlaylists: OfflinePlaylist[];
config: AppConfig;
appVersion: string;
stats: AppStats;
pwaUrl: string;
}
export interface BackendCallbacks {
main: {
AppState: {
GetAppConfig: () => Promise<any>;
SelectDirectory: () => Promise<string>
IsSupportedUrl: (url: string) => Promise<boolean>;
AddToDownload: (url: string, isFromClipboard: boolean) => Promise<any>;
StartDownload: (entry: Entry) => Promise<any>;
ReadSettingBoolValue: (name: string) => Promise<any>;
ReadSettingValue: (name: string) => Promise<any>;
SaveSettingBoolValue: (name: string, val: boolean) => Promise<any>;
SaveSettingValue: (name: string, val: string) => Promise<any>;
RemoveEntry: (entry: Entry) => Promise<any>;
AddToConvertQueue: (entry: Entry) => Promise<any>;
IsFFmpegInstalled: () => Promise<boolean>;
OpenUrl: (url: string) => Promise<any>;
ReloadNewLanguage: () => Promise<void>;
Update: (restart: boolean) => Promise<any>;
ShowWindow: () => Promise<any>;
ForceQuit: () => Promise<any>;
},
OfflinePlaylistService: {
CreateNewPlaylist: (name: string) => Promise<OfflinePlaylist>;
RemovePlaylist: (uuid: string) => Promise<boolean>;
RemoveTrackFromPlaylist: (tid: string, playlist: OfflinePlaylist) => Promise<OfflinePlaylist>;
AddTrackToPlaylist: (playlists: OfflinePlaylist[]) => Promise<boolean>;
ExportPlaylist: (uuid: string, path: string) => Promise<string>;
},
NgrokService: {
IsNgrokInstalled: () => Promise<boolean>;
}
}
}

9
gui_wails03_learn/frontend/src/app/models/entry.ts

@ -0,0 +1,9 @@
import { Playlist } from "./playlist";
import { Track } from "./track";
export interface Entry {
playlist: Playlist;
source: 'youtube' | 'spotify';
track: Track;
type: 'track' | 'playlist';
}

6
gui_wails03_learn/frontend/src/app/models/index.ts

@ -0,0 +1,6 @@
export * from './app-state';
export * from './entry';
export * from './playlist';
export * from './track';
export * from './update-release';
export * from './ngrok';

30
gui_wails03_learn/frontend/src/app/models/ngrok.ts

@ -0,0 +1,30 @@
export type NgrokStateEventPayload = NgrokState;
export interface NgrokState {
status: 'running' | 'error' | 'timeout' | 'killed';
errCode: string;
url: string
}
export const getQrcodeData = (ngrokUrl: string): string => {
const pwa = window.APP_STATE.pwaUrl;
const config = window.APP_STATE.config;
const url = new URL(pwa);
url.searchParams.append("url", ngrokUrl);
if(config.PublicServer.Ngrok.Auth.Enabled) {
url.searchParams.append("username", config.PublicServer.Ngrok.Auth.Username);
url.searchParams.append("password", config.PublicServer.Ngrok.Auth.Password);
} else {
url.searchParams.delete("username");
url.searchParams.delete("password");
}
if(config.PublicServer.VerifyAppKey) {
url.searchParams.append("api_key", config.PublicServer.AppKey);
} else {
url.searchParams.delete("api_key");
}
return url.toString();
}

5
gui_wails03_learn/frontend/src/app/models/offline-playlist.ts

@ -0,0 +1,5 @@
export interface OfflinePlaylist {
uuid: string;
name: string;
tracksIds: string[];
}

8
gui_wails03_learn/frontend/src/app/models/playlist.ts

@ -0,0 +1,8 @@
export interface Playlist {
id: string;
count: number;
name: string;
thumbnail: string;
tracksIds: string[];
url: string
}

19
gui_wails03_learn/frontend/src/app/models/track.ts

@ -0,0 +1,19 @@
export interface Track {
id: string;
author: string;
downloadProgress: number;
duration: number;
filesize: number;
name: string;
playlistId: string;
status: "pending" | "processing" | "downloading" | "downloaded" | "failed";
statusError: string;
thumbnails: string[];
isConvertedToMp3: boolean;
converting: {
status: "converting" | "converted" | "queued" | "failed";
error: string;
attempts: number;
}
url: string;
}

33
gui_wails03_learn/frontend/src/app/models/update-release.ts

@ -0,0 +1,33 @@
export type ReleaseEventPayload = GithubRelease;
interface GithubRelease {
tag_name: string;
body: string;
assets: {
name: string;
browser_download_url: string;
}[];
created_at: string;
}
export class UpdateRelease {
Version: string;
Changelog: string;
Assets: {
Name: string;
Url: string;
}[];
CreatedAt: string;
OldVersion: string;
constructor(json: GithubRelease) {
this.Version = json.tag_name;
this.Changelog = json.body;
this.Assets = json.assets.map(asset => ({ Name: asset.name, Url: asset.browser_download_url }));
this.CreatedAt = json.created_at;
}
static fromJSON(json: GithubRelease): UpdateRelease {
return new this(json)
}
}

186
gui_wails03_learn/frontend/src/app/pages/home/home.component.html

@ -0,0 +1,186 @@
<header fxLayout="row wrap" fxLayoutAlign="space-between center" fxLayoutGap="10px">
<mat-icon class="clickable white" title="Open left menu" (click)="drawer.toggle()" *ngIf="mobileQuery.matches">menu</mat-icon>
<div class="input-wrapper paste" #pasteWrapper *ngIf="!isClipboardWatchEnabled">
<div class="input" fxFlex fxLayout="row" fxLayoutAlign="start center">
<mat-icon class="clickable" (click)="addToDownload()" title="Download from clipboard">content_paste</mat-icon>
<input
#pasteInput
[formControl]="urlInput"
placeholder="{{ 'HOME.PASTE_TRACK_URL' | translate }}"
(focus.silent)="pasteWrapper.classList.add('focused')"
(blur.silent)="pasteWrapper.classList.remove('focused')"
(keydown.enter)="addToDownload()"
/>
</div>
</div>
<div fxLayout="row" fxLayoutAlign="center center" fxLayoutGap="4px" fxFlex>
<div class="input-wrapper">
<div class="input" fxFlex fxLayout="row" fxLayoutAlign="start center">
<mat-icon>search</mat-icon>
<input #searchNativeInput [formControl]="searchInput" placeholder="{{ 'HOME.SEARCH' | translate }}">
<mat-icon
*ngIf="searchInput.value && searchInput.value !== ''"
class="clear-search"
(click)="clearSearch($event)"
title="{{ 'HOME.CLOSE' | translate }}">
close
</mat-icon>
</div>
</div>
<mat-icon class="clickable white" title="Settings" (click)="openSettings()">settings</mat-icon>
<mat-icon
class="clickable white"
title="{{ 'HOME.NEW_UPDATE_AVAILABLE' | translate }}"
matBadge="*"
matBadgePosition="after"
matBadgeOverlap="false"
matBadgeSize="small"
(click)="openUpdate()"
*ngIf="newUpdateInfo">update</mat-icon>
<mat-icon class="clickable white" svgIcon="qrcode_scan" title="Qrcode" (click)="openSettings('public_server')" *ngIf="isNgrokRunning"></mat-icon>
</div>
<language-switcher></language-switcher>
</header>
<mat-drawer-container class="left-sidenav" [hasBackdrop]="mobileQuery.matches">
<mat-drawer [autoFocus]="false" [opened]="!mobileQuery.matches" #drawer [mode]="mobileQuery.matches ? 'over' : 'side'">
<div class="offline-playlists">
<div class="title" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="6px">
<mat-icon>queue_music</mat-icon>
<span fxFlex>{{ 'HOME.PLAYLISTS.LABEL' | translate }}</span>
<span>({{ offlinePlaylists.length }})</span>
</div>
<hr />
<div class="playlists" fxLayout="column" fxLayoutGap="8px">
<div class="playlist" [class.playing]="isPlaylistDetailOpen(p)" fxLayout="column" fxLayoutGap="8px" *ngFor="let p of offlinePlaylists">
<div fxLayout="row" fxLayoutAlign="space-between center">
<span class="name clickable" fxFlex>
{{ p.name }} ({{ p.tracksIds?.length || 0 }})
</span>
<span class="actions" fxLayout="row" fxLayoutGap="4px">
<mat-icon class="clickable" title="{{ 'HOME.PLAYLISTS.PLAYBACK' | translate }}" (click)="playbackPlaylist(p)" *ngIf="!isPlaylistDetailOpen(p) || !isPlaylistInPlayback">play_circle_filled</mat-icon>
<mat-icon class="clickable" title="{{ 'HOME.PLAYLISTS.PAUSE' | translate }}" (click)="stopPlaylist(p)" *ngIf="isPlaylistDetailOpen(p) && isPlaylistInPlayback">pause</mat-icon>
<mat-icon class="clickable" title="{{ 'HOME.PLAYLISTS.EXPORT' | translate }}" (click)="exportPlaylist(p)">folder</mat-icon>
<mat-icon class="clickable" title="{{ 'HOME.PLAYLISTS.REMOVE' | translate }}" (click)="removePlaylist(p)">delete</mat-icon>
</span>
</div>
</div>
</div>
</div>
<div class="footer" fxLayout="column" fxLayoutGap="4px" fxLayoutAlign="start center">
<qr-code [value]="qrCodeData" size="100" errorCorrectionLevel="M" *ngIf="isNgrokRunning"></qr-code>
<mat-icon svgIcon="github" class="clickable" (click)="openGithubProfile()"></mat-icon>
<div class="version clickable" (click)="openReleasePage()">{{ appVersion }}</div>
<div>Made with ❤️ by marcio199226</div>
</div>
</mat-drawer>
<mat-drawer-content>
<router-outlet (activate)="onRouterOutletActivate($event)" (deactivate)="onRouterOutletDeactivate($event)"></router-outlet>
<div class="entries" fxLayout="row wrap" fxLayoutAlign="center start" fxLayoutGap="10px" *ngIf="!isOpenedOfflinePlaylist">
<mat-menu #menu="matMenu">
<ng-template matMenuContent let-entry="entry" let-idx="idx">
<button mat-menu-item (click)="remove(entry, idx)">
<mat-icon>delete</mat-icon>
{{ ('HOME.REMOVE.' + entry.type.toUpperCase()) | translate }}
</button>
<button mat-menu-item (click)="startDownload(entry)" *ngIf="entry.track.status === 'failed'">
<mat-icon>file_download</mat-icon>
Download
</button>
<button mat-menu-item (click)="convertTrack(entry)" *ngIf="entry.track.converting.status === 'failed'">
<mat-icon>file_download</mat-icon>
Convert to mp3
</button>
<button mat-menu-item (click)="openInYt(entry.track.url)">
<mat-icon>open_in_browser</mat-icon>
{{ 'HOME.OPEN_ON_YT' | translate }}
</button>
</ng-template>
</mat-menu>
<div
fxFlex
class="entry"
[class.lastOfRow]="(idx + 1) % 4 === 0"
[class.playing]="inPlaybackTrackId === entry.track?.id"
[class.downloading]="entry.track.status === 'downloading'"
[class.converting]="['converting', 'queued'].indexOf(entry.track.converting.status) > -1"
[ngClass]="entry.type"
*ngFor="let entry of filteredEntries; trackBy: trackById; let idx = index;"
(mouseenter.silent)="onMouseEnter($event, entry)"
(mouseleave.silent)="onMouseLeave($event, entry)">
<div class="bg" [ngStyle]="{'background': getBgUrl(entry)}"></div>
<div class="title">{{ entry.type === 'track' ? entry.track.name : entry.playlist.name }}</div>
<div class="errors" *ngIf="entry.track.status === 'failed' || entry.track.converting.status === 'failed'">
<mat-icon
svgIcon="download_failed"
[matTooltip]="entry.track.statusError">
error
</mat-icon>
<mat-icon [matTooltip]="entry.track.converting.error">
error
</mat-icon>
</div>
<div class="fg">
<div class="wrapper">
<ng-container *ngIf="entry.track.status === 'downloaded' && ['converting', 'queued'].indexOf(entry.track.converting.status) === -1">
<mat-icon class="clickable" title="{{ 'HOME.PLAYLISTS.ADD_TRACK' | translate }}" (click)="addToPlaylist(entry)">playlist_add</mat-icon>
<mat-icon class="clickable play" (click)="playback(entry)" title="{{ 'PLAYER.PLAY' | translate }}">play_circle_filled</mat-icon>
<mat-icon class="clickable stop" (click)="stop(entry)" title="{{ 'PLAYER.STOP' | translate }}">stop</mat-icon>
<mat-icon class="clickable"
[matMenuTriggerFor]="menu"
[matMenuTriggerData]="{entry, idx}"
(menuOpened)="menuOpened()"
(menuClosed)="menuClosed()"
title="More">
more_horiz
</mat-icon>
</ng-container>
<ng-container *ngIf="entry.track.status === 'failed'">
<mat-icon class="clickable m-auto"
[matMenuTriggerFor]="menu"
[matMenuTriggerData]="{entry}"
(menuOpened)="menuOpened()"
(menuClosed)="menuClosed()">
more_horiz
</mat-icon>
</ng-container>
<ng-container *ngIf="entry.track.status === 'processing'">
<mat-icon class="clickable m-auto" title="{{ 'HOME.START_DOWNLOAD_TRACK' | translate }}" (click)="startDownload(entry)">
file_download
</mat-icon>
</ng-container>
<ng-container *ngIf="entry.track.status === 'downloading'">
<div class="progress-container">
<div class="spinner-container">
<span>{{ entry.track.downloadProgress }}%</span>
<mat-progress-spinner mode="determinate" [value]="entry.track.downloadProgress"></mat-progress-spinner>
</div>
</div>
</ng-container>
<ng-container *ngIf="entry.track.converting.status === 'converting'">
<div fxLayout="column" fxLayoutAlign="center center" fxLayoutGap="4px" fxFlex>
<span>{{ 'HOME.CONVERTING_MP3' | translate }}</span>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
</ng-container>
<ng-container *ngIf="entry.track.converting.status === 'queued'">
<div fxLayout="column" fxLayoutAlign="center center" fxLayoutGap="4px" fxFlex>
<span>{{ 'HOME.WAITING_TO_CONVERT_MP3' | translate }}</span>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
</ng-container>
</div>
</div>
</div>
<div *ngIf="!isSearching && !filteredEntries.length">
{{ 'HOME.NO_DOWNLOAED_TRACKS' | translate }}
</div>
<div *ngIf="isSearching && !filteredEntries.length">
{{ 'HOME.NO_SEARCH_RESULTS' | translate }}
</div>
</div>
</mat-drawer-content>
</mat-drawer-container>

276
gui_wails03_learn/frontend/src/app/pages/home/home.component.scss

@ -0,0 +1,276 @@
@import "src/app/scss/breakpoints";
@import '~@angular/material/theming';
app-home {
.left-sidenav {
width: 100%;
height: 100%;
background-color: transparent;
.mat-drawer {
width: 250px;
height: calc(100% - 96px);
padding: 20px 10px;
background-color: rgba(0,0,0,0.4);
border-right: none;
color: white;
.mat-drawer-inner-container {
display: flex;
flex-direction: column;
}
.footer {
.version {
&:hover {
text-decoration: underline;
}
}
}
}
.mat-drawer-container {
}
.offline-playlists {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
.title {
font-weight: 200;
margin-bottom: 0px;
}
hr {
margin-top: 0px;
margin-bottom: 12px;
width: 100%;
}
.playlist {
padding-left: 6px;
transition: all 0.3s;
&.playing {
padding: 5px 8px;
border-radius: 8px;
background-color: rgba(0,0,0,0.2);
}
.name {
font-size: 1.1rem;
font-weight: 300;
}
}
}
}
header {
width: 100%;
height: 96px;
padding: 0 16px;
background: rgba(0, 0, 0, 0.4);
.input-wrapper {
width: 100%;
max-width: 480px;
border-radius: 28px;
overflow: hidden;
background: #fff;
@include mat-elevation(1);
&.paste {
transition: width 0.4s;
width: 210px;
max-width: 210px;
&.focused {
position: absolute;
left: 0;
z-index: 9;
width: calc(100% - 32px);
max-width: 100%;
margin: 0 16px;
}
}
.input {
width: 100%;
height: 48px;
line-height: 48px;
padding: 0 18px;
mat-icon {
color: rgba(0,0,0,.87);
}
.clear-search {
cursor: pointer;
}
input {
width: 100%;
height: 48px;
min-height: 48px;
max-height: 48px;
padding: 0 16px;
border: none;
outline: none;
}
}
}
mat-icon {
&.white {
color: white;
}
}
language-switcher {
.mat-form-field {
width: 100px;
}
}
}
.entries {
margin: 20px auto;
padding: 16px;
overflow: auto;
max-height: calc(768px - 96px - 16px);
max-width: 1024px;
.entry {
position: relative;
flex: 1;
background: rgba(0, 0, 0, 0.3);
background-repeat: no-repeat !important;
background-size: cover !important;
min-width: 220px;
max-width: 500px;
height: 250px;
margin-bottom: 10px;
border-radius: 8px;
&.lastOfRow {
margin-right: 0px !important;
}
&.onHover, &.playing, &.downloading, &.converting {
.fg {
display: flex;
width: 100%;
height: 100%;
position: absolute;
z-index: 999;
background: rgba(0,0,0,0.6);
top: 0;
left: 0;
color: #fff;
border-radius: 8px;
}
}
&.playing {
.play {
display: none;
}
}
&:not(.playing) {
.stop {
display: none;
}
}
.bg {
background-repeat: no-repeat !important;
background-size: cover !important;
width: 100%;
height: 100%;
border-radius: 8px;
}
.title {
position: absolute;
bottom: 0;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
text-align: center;
color: #fff;
background: rgba(0,0,0,0.75);
padding: 6px 12px;
display: flex;
align-items: center;
width: 100%;
justify-content: center;
box-sizing: border-box;
min-height: 52px;
}
.errors {
position: absolute;
top: 5px;
left: 0px;
z-index: 1000;
display: flex;
justify-content: space-between;
width: calc(100% - 16px);
padding: 0px 8px;
.mat-icon {
cursor: pointer;
color: #fff;
}
}
.fg {
display: none;
.wrapper {
width: 100%;
display: flex;
align-items: center !important;
justify-content: space-between !important;
padding: 0 16px;
.play, .stop {
font-size: 75px;
height: 75px;
line-height: 75px;
width: 75px;
}
.progress-container {
display: flex;
flex-direction: column-reverse;
align-items: center;
flex: 1;
font-weight: bold;
.spinner-container {
position: relative;
margin-bottom: 1em;
.mat-progress-spinner {
margin: 0 auto;
circle {
stroke: #fff;
}
}
span {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
}
}
}
}
}
}

760
gui_wails03_learn/frontend/src/app/pages/home/home.component.ts

@ -0,0 +1,760 @@
import {
ApplicationRef,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
Inject,
NgZone,
OnDestroy,
OnInit,
TemplateRef,
ViewChild,
ViewEncapsulation,
} from '@angular/core';
import { FormControl } from '@angular/forms';
import { AudioPlayerService } from 'app/components/audio-player/audio-player.service';
import { Track, Entry, UpdateRelease, ReleaseEventPayload, NgrokState, getQrcodeData } from '@models';
import { AppConfig, AppState } from '../../models/app-state';
import * as Wails from '@wailsapp/runtime';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { SettingsComponent, UpdaterComponent } from 'app/components';
import { debounceTime, distinctUntilChanged, filter } from 'rxjs/operators';
import { MatMenu } from '@angular/material/menu';
import { DOCUMENT } from '@angular/common';
import { SnackbarService } from 'app/services/snackbar.service';
import { MediaMatcher } from '@angular/cdk/layout';
import { OfflinePlaylist } from 'app/models/offline-playlist';
import { AddToPlaylistComponent, CreatePlaylistComponent } from 'app/components/playlist';
import { ConfirmationDialogComponent } from 'app/components/confirmation-dialog/confirmation-dialog.component';
import to from 'await-to-js';
import { ActivatedRoute, Router } from '@angular/router';
import { MatDrawer } from '@angular/material/sidenav';
import { minMax } from 'app/common/fn';
import { OfflinePlaylistComponent } from '../offline-playlist/offline-playlist.component';
import { LoaderService } from 'app/services/loader.service';
import { TranslateService } from '@ngx-translate/core';
import { fakeAsync } from '@angular/core/testing';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
})
export class HomeComponent implements OnInit, OnDestroy {
public searchInput: FormControl;
public urlInput: FormControl;
public entries: Entry[] = [];
public filteredEntries: Entry[] = [];
public offlinePlaylists: OfflinePlaylist[] = [];
public onHoverEntry: Entry = null;
public inPlayback: Track = null;
public newUpdateInfo: UpdateRelease = null;
public ngrok: NgrokState = null;
public get inPlaybackTrackId(): string {
if(!this.inPlayback) {
return null;
}
return this.inPlayback.id;
}
public get isClipboardWatchEnabled(): boolean {
return window.APP_STATE.config.ClipboardWatch;
}
public get isSearching(): boolean {
if(!this.searchNativeInput) {
return false;
}
return this.searchNativeInput.nativeElement === document.activeElement || !!this.searchInput.value;
}
public get isOpenedOfflinePlaylist(): boolean {
return this._route.children.length !== 0;
}
public get isPlaylistInPlayback(): boolean {
if(!this._offlinePlaylistComponent) {
return false;
}
return this._offlinePlaylistComponent.inPlaybackTrackId !== null;
}
public get appVersion(): string {
return window.APP_STATE.appVersion;
}
public get isNgrokRunning(): boolean {
if(!this.ngrok) {
return false;
}
return this.ngrok.status === 'running';
}
public get qrCodeData(): string {
return getQrcodeData(this.ngrok.url);
}
@ViewChild('searchNativeInput')
public searchNativeInput: ElementRef<HTMLInputElement> = null;
@ViewChild('pasteWrapper')
public pasteWrapper: ElementRef<HTMLDivElement> = null;
@ViewChild('pasteInput')
public pasteInput: ElementRef<HTMLInputElement> = null;
@ViewChild('menu')
public matMenu: MatMenu = null;
@ViewChild('drawer')
public drawer: MatDrawer = null;
public menuIsOpened: boolean = false;
public mobileQuery: MediaQueryList;
private _mobileQueryListener: (event: MediaQueryListEvent) => void;
private _offlinePlaylistComponent: OfflinePlaylistComponent = null
constructor(
private _cdr: ChangeDetectorRef,
private _appRef: ApplicationRef,
private _ngZone: NgZone,
@Inject(DOCUMENT) private _document: Document,
private _router: Router,
private _route: ActivatedRoute,
private _dialog: MatDialog,
private _snackbar: SnackbarService,
private _loader: LoaderService,
private _media: MediaMatcher,
private _trans: TranslateService,
private _audioPlayerService: AudioPlayerService
) {
this.searchInput = new FormControl('');
this.urlInput = new FormControl('');
this.mobileQuery = this._media.matchMedia('(max-width: 1024px)');
if(!this.mobileQuery.matches) {
this._document.body.classList.add('fullscreen');
}
this._mobileQueryListener = (e) => {
if(!e.matches) {
this._document.body.classList.add('fullscreen');
} else {
this._document.body.classList.remove('fullscreen');
}
this._cdr.detectChanges();
}
this.mobileQuery.addEventListener("change", this._mobileQueryListener);
}
ngOnInit(): void {
this.entries = window.APP_STATE.entries;
this.filteredEntries = this.entries;
this.offlinePlaylists = window.APP_STATE.offlinePlaylists;
// subscribe to events coming from backend
Wails.Events.On("ytd:track", (payload: Entry) => {
this._ngZone.run(() => {
console.log("ytd:track", payload)
const entry = this.entries.find(e => e.track.id === payload.track.id);
if(entry) {
entry.track = payload.track;
} else {
this.entries.unshift(payload);
}
this._cdr.detectChanges();
});
});
Wails.Events.On("ytd:track:progress", ({ id, progress }) => {
const entry = this.entries.find(e => e.track.id === id);
entry.track.downloadProgress = progress;
this._cdr.detectChanges();
});
// show snackbar when convert queue is full and some track is waiting to be converted
Wails.Events.On("ytd:track:convert:queued", (payload: Entry) => {
this._ngZone.run(() => {
console.log("ytd:track:convert:queued", payload)
this._snackbar.openInfo('HOME.CONVERT_TO_MP3_QUEUED');
const entry = this.entries.find(e => e.track.id === payload.track.id);
entry.track.converting.status = 'queued';
this._cdr.detectChanges();
});
});
Wails.Events.On("ytd:playlist", payload => console.log(payload));
Wails.Events.On("ytd:offline:playlists", offlinePlaylists => {
this._ngZone.run(() => {
console.log("UPDATE offlinePlaylists", offlinePlaylists)
this.offlinePlaylists = offlinePlaylists;
this._cdr.detectChanges();
});
});
Wails.Events.On("ytd:app:config", (config) => {
this._ngZone.run(() => {
window.APP_STATE.config = config;
this._snackbar.openSuccess("SETTINGS.SAVED");
this._cdr.detectChanges();
});
});
Wails.Events.On("ytd:show:dialog:settings", () => {
this._ngZone.run(() => {
this.openSettings();
})
});
Wails.Events.On("ytd:app:update:available", (release: ReleaseEventPayload) => {
this._ngZone.run(() => {
this.newUpdateInfo = UpdateRelease.fromJSON(release);
this._cdr.detectChanges();
})
});
Wails.Events.On("ytd:app:update:changelog", (release: ReleaseEventPayload) => {
this._ngZone.run(() => {
this.newUpdateInfo = UpdateRelease.fromJSON(release);
this.openUpdate();
this._cdr.detectChanges();
})
});
Wails.Events.On("ytd:app:update:apply", (release: ReleaseEventPayload) => {
this._ngZone.run(() => {
this.newUpdateInfo = UpdateRelease.fromJSON(release);
})
});
Wails.Events.On("ytd:ngrok", (ngrok: NgrokState) => {
this._ngZone.run(async () => {
console.log("ytd:ngrok", ngrok)
this.ngrok = ngrok;
if(this.ngrok.status === 'error') {
const transPath = `SETTINGS.TABS.PUBLIC_SERVER.NGROK.ON_STARTUP_ERRORS.${this.ngrok.errCode}`;
const defaultTransPath = `SETTINGS.TABS.PUBLIC_SERVER.NGROK.ON_STARTUP_ERRORS.DEFAULT`;
const transStr = this._trans.instant(transPath);
const exists = transStr !== transPath;
const snackRef = this._snackbar.openWarning(
exists ? transPath : defaultTransPath, 'SETTINGS.TABS.PUBLIC_SERVER.NGROK.ERROR_DETAILS_BTN',
{ duration: 10000 },
{ errCode: this.ngrok.errCode }
);
snackRef.onAction().subscribe(async () => {
await window.backend.main.AppState.OpenUrl(`https://ngrok.com/docs/errors/${this.ngrok.errCode.toLowerCase()}`);
});
}
if(this.ngrok.status === 'killed') {
this._snackbar.openWarning('SETTINGS.TABS.PUBLIC_SERVER.NGROK.CRASHED', '', { duration: 10000 });
}
this._cdr.detectChanges();
})
});
// players commands
this._audioPlayerService.onPlayCmdTrack.subscribe(track => {
this.inPlayback = track;
this._cdr.detectChanges();
});
this._audioPlayerService.onStopCmdTrack.subscribe(track => {
console.log("ON STOPPP FROM HOMEEE", track)
this.inPlayback = null;
this._cdr.detectChanges();
});
this._audioPlayerService.onPrevTrackCmd.pipe(filter(track => track !== null)).subscribe(currentTrack => {
const trackIdx = this.entries.findIndex(e => e.track && e.track.id === currentTrack.id);
if(trackIdx - 1 < 0) {
this._snackbar.openWarning("PLAYER.CANNOT_PLAY_PREV")
return;
}
let idx = 1;
let prevTrack;
do {
prevTrack = this.entries[trackIdx - idx].track;
idx++;
} while(
window.wails.System.Platform() !=='darwin' ||
window.wails.System.Platform() ==='darwin' && !prevTrack.isConvertedToMp3
)
this.inPlayback = prevTrack;
this._audioPlayerService.onPlaybackTrack.next(prevTrack);
});
this._audioPlayerService.onNextTrackCmd.pipe(filter(track => track !== null)).subscribe(currentTrack => {
const trackIdx = this.entries.findIndex(e => e.track && e.track.id === currentTrack.id);
if(trackIdx + 1 >= this.entries.length) {
this._snackbar.openWarning("PLAYER.CANNOT_PLAY_NEXT")
return;
}
let idx = 1;
let nextTrack;
do {
nextTrack = this.entries[trackIdx + idx].track;
idx++;
} while(
window.wails.System.Platform() !=='darwin' ||
window.wails.System.Platform() ==='darwin' && !nextTrack.isConvertedToMp3
)
this.inPlayback = nextTrack;
this._audioPlayerService.onPlaybackTrack.next(nextTrack);
});
this._audioPlayerService.onShuffleTrackCmd.pipe(filter(track => track !== null)).subscribe(currentTrack => {
const tracks = this.entries.filter(e => e.type === 'track');
const idx = minMax(0, tracks.length)
this.inPlayback = tracks[idx].track;
this._audioPlayerService.onPlaybackTrack.next(tracks[idx].track);
});
this._onSearch();
}
onRouterOutletActivate(cmp: any): void {
this._offlinePlaylistComponent = cmp;
this._cdr.detectChanges();
}
onRouterOutletDeactivate(e: any): void {
this._offlinePlaylistComponent = null;
this._cdr.detectChanges();
}
clearSearch(event: any): void {
this.searchNativeInput.nativeElement.focus();
setTimeout(() => this.searchInput.setValue(''))
}
openSettings(tab?: string): MatDialogRef<SettingsComponent, any> {
const dialogRef = this._dialog.open(SettingsComponent, {
panelClass: ['settings-dialog', 'with-header-dialog'],
width: '700px',
maxHeight: '700px',
data: { tab, config: window.APP_STATE.config, isNgrokRunning: this.isNgrokRunning, ngrok: this.ngrok }
});
dialogRef.afterClosed().subscribe(async (result: { config: AppConfig }) => {
if(!result) {
return;
}
const { config } = result;
// put all for into try/catch block and open snackbar with error if something fails
try {
for (const [key, value] of Object.entries(config)) {
switch(key) {
case 'BaseSaveDir':
await window.backend.main.AppState.SaveSettingValue(key, value as string);
break;
case 'ClipboardWatch':
case 'DownloadOnCopy':
case 'ConcurrentDownloads':
case 'ConcurrentPlaylistDownloads':
case 'ConvertToMp3':
case 'CleanWebmFiles':
await window.backend.main.AppState.SaveSettingBoolValue(key, value as boolean);
break;
case 'MaxParrallelDownloads':
await window.backend.main.AppState.SaveSettingValue(key, `${value}`);
break;
// struct configs
case 'Telegram':
case 'PublicServer':
await window.backend.main.AppState.SaveSettingValue(key, JSON.stringify(value));
break;
}
}
// if no errors save new config without retrieve it from backend again (we load app state only once when app is launched)
window.APP_STATE.config = config;
this._snackbar.openSuccess("SETTINGS.SAVED");
Wails.Events.Emit("ytd:app:tray:update")
this._cdr.detectChanges();
} catch(e) {
console.log(e)
this._snackbar.openError("SETTINGS.KO");
}
});
return dialogRef
}
openUpdate(): void {
const dialogRef = this._dialog.open(UpdaterComponent, {
autoFocus: false,
panelClass: ['updater-dialog', 'with-header-dialog'],
width: '600px',
maxHeight: '700px',
data: { release: this.newUpdateInfo, oldVersion: window.APP_STATE.appVersion }
});
dialogRef.afterClosed().subscribe(async (result) => {
if(!result) {
return;
}
const { action } = result;
switch(action) {
case 'UpdateAndReplace':
await window.backend.main.AppState.Update(false);
}
});
}
trackById(idx: number, entry: Entry): string {
if(entry.playlist.id) {
return entry.playlist.id;
}
return entry.track.id;
}
getBgUrl(entry: Entry): string {
return `url(${entry.track.thumbnails ? entry.track.thumbnails[4] ? entry.track.thumbnails[4] : entry.track.thumbnails[3] : entry.playlist.thumbnail})`;
}
onMouseEnter($event: Event, entry: Entry): void {
this.onHoverEntry = entry;
if(this.menuIsOpened) {
return;
}
($event.target as HTMLDivElement).classList.add('onHover')
}
onMouseLeave($event: Event, entry: Entry): void {
this.onHoverEntry = null;
if(this.menuIsOpened) {
return;
}
($event.target as HTMLDivElement).classList.remove('onHover')
}
playback(entry: Entry): void {
if(window.wails.System.Platform() ==='darwin' && !entry.track.isConvertedToMp3) {
const ref = this._snackbar.openWarning("SETTINGS.MACOS_MISSING_CONVERT_TO_MP3_OPTIONS", "SETTINGS.OPEN_DIALOG_BTN")
ref.onAction().subscribe(() => this.openSettings());
return;
}
this.inPlayback = entry.track;
this._audioPlayerService.onPlaybackTrack.next(entry.track);
}
stop(entry: Entry): void {
this.inPlayback = null;
this._audioPlayerService.onStopTrack.next(entry.track);
}
menuOpened(): void {
this.menuIsOpened = true;
}
menuClosed(): void {
this.menuIsOpened = false;
const onHoveredEntry = this._document.querySelector('.entry.onHover');
if(onHoveredEntry && !this.onHoverEntry) {
onHoveredEntry.classList.toggle('onHover')
}
}
async addToDownload(): Promise<void> {
const url = this.urlInput.value;
if(!url) {
return;
}
const isSupported = await window.backend.main.AppState.IsSupportedUrl(url);
if(!isSupported) {
this._snackbar.openWarning("SETTINGS.MANUAL_MODE.UNSUPPORTED_URL");
return
}
try {
await window.backend.main.AppState.AddToDownload(url, false)
this.urlInput.setValue('');
this.pasteInput.nativeElement.blur();
this.pasteWrapper.nativeElement.classList.remove("focused");
this._snackbar.openSuccess("SETTINGS.MANUAL_MODE.TRACK_ADDED");
} catch(e) {
console.log(e);
this._snackbar.openError("SETTINGS.MANUAL_MODE.TRACK_ADD_KO");
}
}
async startDownload(entry: Entry): Promise<void> {
try {
await window.backend.main.AppState.StartDownload(entry);
this._snackbar.openSuccess("HOME.STARTED_DOWNLOAD_TRACK");
} catch(e) {
this._snackbar.openError(e);
}
}
async convertTrack(entry: Entry): Promise<void> {
try {
await window.backend.main.AppState.AddToConvertQueue(entry);
} catch(e) {
this._snackbar.openError(e);
}
}
addToPlaylist(entry: Entry): void {
const foundInPlaylists = this.offlinePlaylists.filter(op => op.tracksIds.indexOf(entry.track.id) > -1);
const dialogRef = this._dialog.open(AddToPlaylistComponent, {
autoFocus: false,
panelClass: ['add-to-playlist-dialog', 'with-header-dialog'],
maxWidth: '500px',
maxHeight: '500px',
data: { entry, playlists: this.offlinePlaylists, foundInPlaylists }
});
dialogRef.afterClosed().subscribe(async (result) => {
if(!result) {
return;
}
const { action, selectedPlaylists } = result;
console.log(selectedPlaylists)
if(!action) {
const playlists: OfflinePlaylist[] = selectedPlaylists.map((uuid: string) => this.offlinePlaylists.find(op => op.uuid === uuid))
playlists.forEach(p => p.tracksIds.push(entry.track.id));
const [err, added] = await to(window.backend.main.OfflinePlaylistService.AddTrackToPlaylist(playlists))
if(err) {
this._snackbar.openError("PLAYLISTS.ADD_TRACK_KO");
return;
}
this._snackbar.openSuccess("PLAYLISTS.ADD_TRACK_OK");
}
switch(action) {
case 'createNew':
this.createPlaylist();
}
});
}
createPlaylist(): void {
const dialogRef = this._dialog.open(CreatePlaylistComponent, {
panelClass: ['create-playlist-dialog', 'with-header-dialog'],
width: '300px',
maxHeight: '500px',
data: { playlists: this.offlinePlaylists }
});
dialogRef.afterClosed().subscribe(async (result) => {
if(!result) {
return;
}
const { playlist } = result;
const [err, createdPlaylist] = await to(window.backend.main.OfflinePlaylistService.CreateNewPlaylist(playlist.name))
if(err) {
this._snackbar.openError("PLAYLISTS.CREATED_KO");
return
}
this._snackbar.openSuccess("PLAYLISTS.CREATED_OK");
// notify go backend that new playlist has been created, this updates backend state.offlinePlaylists
// and then emit ytd:offline:playlists back to fe with offlinePlaylists to sync state between each other
Wails.Events.Emit("ytd:offline:playlists:created");
});
}
removePlaylist(playlist: OfflinePlaylist): void {
const dialogRef = this._dialog.open(ConfirmationDialogComponent, {
autoFocus: false,
panelClass: ['with-header-dialog'],
width: '300px',
data: {
title: "PLAYLISTS.REMOVE_DIALOG.TITLE",
text: this._trans.instant("PLAYLISTS.REMOVE_DIALOG.DESC", { name: playlist.name })
}
});
dialogRef.afterClosed().subscribe(async (result) => {
if(!result) {
return;
}
// call remove from backend and update list
const [err, isRemoved] = await to(window.backend.main.OfflinePlaylistService.RemovePlaylist(playlist.uuid))
if(err || !isRemoved) {
console.log(err, isRemoved)
this._snackbar.openError("PLAYLISTS.REMOVE_DIALOG.KO");
return
}
this._snackbar.openSuccess("PLAYLISTS.REMOVE_DIALOG.OK");
// notify go backend that playlist has been deleted, this updates backend state.offlinePlaylists
// and then emit ytd:offline:playlists back to fe with offlinePlaylists to sync state between each other
Wails.Events.Emit("ytd:offline:playlists:removed");
});
}
async exportPlaylist(playlist: OfflinePlaylist): Promise<void> {
if(!playlist.tracksIds.length) {
this._snackbar.openWarning("HOME.PLAYLISTS.CANNOT_EXPORT_EMPTY");
return;
}
const [err, dirResult] = await to<string, Error>(window.wails.Dialog.Open({
DefaultDirectory: playlist.name,
AllowFiles: false,
CanCreateDirectories: true,
AllowDirectories: true,
Title: this._trans.instant("HOME.PLAYLISTS.CHOOSE_EXPORT_DIRECTORY"),
}))
const dir: string[] = JSON.parse(dirResult);
if(err) {
console.log('err, dirResult', err, dirResult)
this._snackbar.openError("HOME.PLAYLISTS.CHOOSE_EXPORT_DIRECTORY_KO", null, null, { error: err });
return;
}
if(!dir.length) {
return;
}
this._loader.show("HOME.PLAYLISTS.LOADER.EXPORTING");
const [exportErr, exportResult] = await to(window.backend.main.OfflinePlaylistService.ExportPlaylist(playlist.uuid, dir[0]))
this._loader.hide();
if(exportErr) {
console.log('exportPlaylist go call results in error', exportErr, exportResult);
this._snackbar.openError("HOME.PLAYLISTS.EXPORTED_KO", null, null, { error: exportErr });
return;
}
this._snackbar.openSuccess("HOME.PLAYLISTS.EXPORTED_OK");
}
async playbackPlaylist(playlist: OfflinePlaylist): Promise<void> {
if(!playlist.tracksIds.length) {
this._snackbar.openWarning("PLAYLISTS.CANNOT_PLAYBACK_EMPTY");
return;
}
if(this.mobileQuery.matches) {
await this.drawer.close();
}
const tracks = this.entries.filter(e => e.type === 'track' && playlist.tracksIds.indexOf(e.track.id) > -1).map(e => e.track);
this._router.navigate(['playlist', playlist.uuid], { relativeTo: this._route, state: { playlist, tracks } });
this._audioPlayerService.onPlaybackTrack.next(tracks[0]);
this._cdr.detectChanges();
}
stopPlaylist(playlisy: OfflinePlaylist): void {
this._offlinePlaylistComponent.stop();
this._cdr.detectChanges();
}
isPlaylistDetailOpen(playlist: OfflinePlaylist): boolean {
if(!this._route.children.length) {
return false;
}
return this._route.children[0].snapshot.paramMap.get('playlist') === playlist.uuid;
}
async removeTrackFromPlaylist(trackId: string, playlist: OfflinePlaylist): Promise<void> {
const [err, updatedPlaylist] = await to(window.backend.main.OfflinePlaylistService.RemoveTrackFromPlaylist(trackId, playlist))
if(err) {
console.log(err, updatedPlaylist)
this._snackbar.openError("PLAYLISTS.REMOVE_TRACK.KO");
return
}
this._snackbar.openSuccess("PLAYLISTS.REMOVE_TRACK.OK");
// notify go backend that playlist has been deleted, this updates backend state.offlinePlaylists
// and then emit ytd:offline:playlists back to fe with offlinePlaylists to sync state between each other
Wails.Events.Emit("ytd:offline:playlists:removedTrack");
}
async remove(entry: Entry, i: number): Promise<void> {
try {
await window.backend.main.AppState.RemoveEntry(entry);
this._snackbar.openSuccess(`HOME.REMOVED.OK_${entry.type.toUpperCase()}`);
const idx = this.entries.findIndex(e => {
if(entry.type === 'playlist') {
return e.playlist.id === entry.playlist.id;
}
return e.track.id === entry.track.id;
})
this.entries.splice(idx, 1);
this.filteredEntries = this.entries;
//this._cdr.detectChanges();
} catch(e) {
this._snackbar.openError(`HOME.REMOVED.KO_${entry.type.toUpperCase()}`);
}
}
async openInYt(url: string): Promise<void> {
await window.backend.main.AppState.OpenUrl(url);
}
async openGithubProfile(): Promise<void> {
await window.backend.main.AppState.OpenUrl('https://github.com/marcio199226/ytd');
}
async openReleasePage(): Promise<void> {
await window.backend.main.AppState.OpenUrl(`https://github.com/marcio199226/ytd/releases/tag/${this.appVersion}`);
}
private _onSearch(): void {
this.searchInput.valueChanges
.pipe(
debounceTime(300),
distinctUntilChanged(),
)
.subscribe((searchText: string) => {
if(!!searchText === false) {
this.filteredEntries = this.entries;
this._cdr.detectChanges();
return;
}
searchText = searchText.toLowerCase();
this.filteredEntries = this.entries.filter(e => {
if(e.type === 'playlist') {
return e.playlist.name.toLowerCase().includes(searchText);
}
return e.track.author.toLowerCase().includes(searchText) || e.track.name.toLowerCase().includes(searchText)
});
this._cdr.detectChanges();
});
}
ngOnDestroy(): void {
this.mobileQuery.removeEventListener("change", this._mobileQueryListener);
}
}

2
gui_wails03_learn/frontend/src/app/pages/index.ts

@ -0,0 +1,2 @@
export * from './home/home.component';
export * from './offline-playlist/offline-playlist.component';

36
gui_wails03_learn/frontend/src/app/pages/offline-playlist/offline-playlist.component.html

@ -0,0 +1,36 @@
<mat-menu #menu="matMenu">
<ng-template matMenuContent let-playlist="playlist" let-track="track">
<button mat-menu-item (click)="removeTrackFromPlaylist(track.id, playlist)">
<mat-icon>delete</mat-icon>
{{ 'PLAYLISTS.REMOVE_TRACK' | translate }}
</button>
<button mat-menu-item>
<mat-icon>open_in_browser</mat-icon>
{{ 'HOME.OPEN_IN_YT' | translate }}
</button>
</ng-template>
</mat-menu>
<div class="tracks" fxLayout="column" fxLayoutAlign="start start">
<div class="track" [class.playing]="inPlaybackTrackId === track.id" [class.paused]="inPauseTrack?.id === track.id" fxLayout="row" *ngFor="let track of tracks; trackBy: trackById; let idx = index">
<div class="idx">{{ idx + 1 }}.</div>
<div class="thumbnail">
<img [src]="track.thumbnails[3]" />
<img class="equalizer" src="http://localhost:8080/static/frontend/dist/assets/playing-wave.gif" />
<mat-icon class="play clickable" (click)="playback(track)">play_circle_filled</mat-icon>
<mat-icon class="stop clickable" (click)="stop(track)">pause</mat-icon>
</div>
<div class="name" fxLayout="column" fxLayoutGap="8px" fxLayoutAlign="center start" fxFlex="1 1 100%">
<div class="author">{{ track.author }}</div>
<div class="title">{{ track.name }}</div>
</div>
<div class="time"></div>
<div class="menu" fxLayoutAlign="center center">
<mat-icon class="clickable"
[matMenuTriggerFor]="menu"
[matMenuTriggerData]="{playlist, track, idx}"
title="More">
more_horiz
</mat-icon>
</div>
</div>
</div>

133
gui_wails03_learn/frontend/src/app/pages/offline-playlist/offline-playlist.component.scss

@ -0,0 +1,133 @@
@import "src/app/scss/breakpoints";
@import '~@angular/material/theming';
app-offline-playlist {
.tracks {
color: white;
height: 100%;
max-height: calc(100% - 96px - 96px);
overflow-y: scroll;
.track {
position: relative;
width: 100%;
background: rgba(0, 0, 0, 0.6);
padding: 8px 12px;
border-bottom: 1px inset rgba(255, 255, 255, 0.15);
&:hover {
background: rgba(255, 255, 255, 0.15);
.thumbnail {
background-color: rgba(0, 0, 0, 0.6);
z-index: -1;
opacity: 1 !important;
}
&.playing {
.thumbnail {
z-index: 0;
}
.stop {
display: block;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 35px;
height: 35px;
font-size: 35px;
line-height: 35px;
}
.equalizer {
display: none;
}
}
&:not(.playing) {
.thumbnail {
z-index: 0;
img:not(.equalizer) {
opacity: 0.25;
}
}
.play {
display: block !important;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 35px;
height: 35px;
font-size: 35px;
line-height: 35px;
}
}
}
&.playing {
.thumbnail {
img:not(.equalizer) {
opacity: 0.25;
}
}
.equalizer {
display: block;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 35px;
height: 35px;
}
}
&:not(.playing) {
.equalizer {
display: none;
}
}
&.paused {
.play {
display: block !important;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 35px;
height: 35px;
font-size: 35px;
line-height: 35px;
}
}
.idx {
margin: auto 15px auto 0px;
}
.thumbnail {
position: relative;
margin-right: 12px;
border-radius: 8px;
height: 70px;
img:not(.equalizer) {
width: 70px;
height: 70px;
border-radius: 8px;
object-fit: cover;
}
.mat-icon {
display: none;
}
}
}
}
}

190
gui_wails03_learn/frontend/src/app/pages/offline-playlist/offline-playlist.component.ts

@ -0,0 +1,190 @@
import {
ApplicationRef,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
Inject,
NgZone,
OnDestroy,
OnInit,
ViewEncapsulation,
} from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { AudioPlayerService } from 'app/components/audio-player/audio-player.service';
import { Track, Entry, UpdateRelease, ReleaseEventPayload } from '@models';
import wailsapp__runtime, * as Wails from '@wailsapp/runtime';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { debounceTime, distinctUntilChanged, filter, takeUntil } from 'rxjs/operators';
import { MatMenu } from '@angular/material/menu';
import { DOCUMENT } from '@angular/common';
import { SnackbarService } from 'app/services/snackbar.service';
import { OfflinePlaylist } from 'app/models/offline-playlist';
import { ConfirmationDialogComponent } from 'app/components/confirmation-dialog/confirmation-dialog.component';
import to from 'await-to-js';
import { Subject, Subscription } from 'rxjs';
import { minMax } from 'app/common/fn';
@Component({
selector: 'app-offline-playlist',
templateUrl: './offline-playlist.component.html',
styleUrls: ['./offline-playlist.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
})
export class OfflinePlaylistComponent implements OnInit, OnDestroy {
public playlist: OfflinePlaylist = null;
public tracks: Track[] = [];
public inPlayback: Track = null;
public inPauseTrack: Track = null;
public get inPlaybackTrackId(): string {
if(!this.inPlayback) {
return null;
}
return this.inPlayback.id;
}
private _unsubscribe: Subject<any> = new Subject();
constructor(
private _cdr: ChangeDetectorRef,
private _appRef: ApplicationRef,
private _ngZone: NgZone,
@Inject(DOCUMENT) private _document: Document,
private _router: Router,
private _route: ActivatedRoute,
private _dialog: MatDialog,
private _snackbar: SnackbarService,
private _audioPlayerService: AudioPlayerService
) {
const { playlist, tracks } = this._router.getCurrentNavigation().extras.state;
this.playlist = playlist;
this.tracks = tracks;
this.inPlayback = this.tracks[0];
}
ngOnInit(): void {
const audioPlayer = this._document.querySelector('audio-player .player');
audioPlayer.setAttribute("view", "playlist");
// players commands
this._audioPlayerService.onPlayCmdTrack.pipe(filter(track => track !== null), takeUntil(this._unsubscribe)).subscribe(track => {
this.inPlayback = track;
this.inPauseTrack = null;
this._cdr.detectChanges();
});
this._audioPlayerService.onStopCmdTrack.pipe(filter(track => track !== null), takeUntil(this._unsubscribe)).subscribe(track => {
console.log("ON STOPPPPP", track)
this.inPlayback = null;
this.inPauseTrack = track;
this._cdr.detectChanges();
});
this._audioPlayerService.onPrevTrackCmd.pipe(filter(track => track !== null), takeUntil(this._unsubscribe)).subscribe(currentTrack => {
const trackIdx = this.tracks.findIndex(track => track.id === currentTrack.id);
if(trackIdx - 1 < 0) {
this._snackbar.openWarning("PLAYER.CANNOT_PLAY_PREV")
return;
}
let idx = 1;
let prevTrack;
do {
prevTrack = this.tracks[trackIdx - idx];
idx++;
} while(
window.wails.System.Platform() !=='darwin' ||
window.wails.System.Platform() ==='darwin' && !prevTrack.isConvertedToMp3
)
this.inPlayback = prevTrack;
this._audioPlayerService.onPlaybackTrack.next(prevTrack);
this._cdr.detectChanges();
});
this._audioPlayerService.onNextTrackCmd.pipe(filter(track => track !== null), takeUntil(this._unsubscribe)).subscribe(currentTrack => {
const trackIdx = this.tracks.findIndex(track => track.id === currentTrack.id);
if(trackIdx + 1 >= this.tracks.length) {
this._snackbar.openWarning("PLAYER.CANNOT_PLAY_NEXT")
return;
}
let idx = 1;
let nextTrack;
do {
nextTrack = this.tracks[trackIdx + idx];
idx++;
} while(
window.wails.System.Platform() !=='darwin' ||
window.wails.System.Platform() ==='darwin' && !nextTrack.isConvertedToMp3
)
this.inPlayback = nextTrack;
this._audioPlayerService.onPlaybackTrack.next(nextTrack);
this._cdr.detectChanges();
});
this._audioPlayerService.onShuffleTrackCmd.pipe(filter(track => track !== null), takeUntil(this._unsubscribe)).subscribe(currentTrack => {
const idx = minMax(0, this.tracks.length)
this.inPlayback = this.tracks[idx];
this._audioPlayerService.onPlaybackTrack.next(this.tracks[idx]);
this._cdr.detectChanges();
});
this._audioPlayerService.onClosePlayer.pipe(filter(v => v !== null), takeUntil(this._unsubscribe)).subscribe(() => {
this.close();
})
}
playback(track: Track): void {
if(window.wails.System.Platform() ==='darwin' && !track.isConvertedToMp3) {
const ref = this._snackbar.openWarning("SETTINGS.MACOS_MISSING_CONVERT_TO_MP3_OPTIONS")
return;
}
this.inPlayback = track;
this.inPauseTrack = null;
this._audioPlayerService.onPlaybackTrack.next(track);
}
stop(track?: Track): void {
this._audioPlayerService.onStopTrack.next(this.inPlayback);
this.inPlayback = null;
this.inPauseTrack = track;
}
async removeTrackFromPlaylist(trackId: string, playlist: OfflinePlaylist): Promise<void> {
const [err, updatedPlaylist] = await to(window.backend.main.OfflinePlaylistService.RemoveTrackFromPlaylist(trackId, playlist))
if(err) {
console.log(err, updatedPlaylist)
this._snackbar.openError("PLAYLISTS.REMOVE_TRACK.KO");
return
}
this._snackbar.openSuccess("PLAYLISTS.REMOVE_TRACK.OK");
// notify go backend that playlist has been deleted, this updates backend state.offlinePlaylists
// and then emit ytd:offline:playlists back to fe with offlinePlaylists to sync state between each other
Wails.Events.Emit("ytd:offline:playlists:removedTrack");
}
trackById(idx: number, track: Track): string {
return track ? track.id : undefined;
}
close(): void {
this._router.navigate(['home']);
}
ngOnDestroy(): void {
const audioPlayer = this._document.querySelector('audio-player .player');
audioPlayer.removeAttribute("view");
this._unsubscribe.next();
this._unsubscribe.complete();
}
}

38
gui_wails03_learn/frontend/src/app/scss/breakpoints.scss

@ -0,0 +1,38 @@
// Media step breakpoint mixin based on Angular Material lib
$breakpoints: (
xs: 'screen and (max-width: 599px)',
sm: 'screen and (min-width: 600px) and (max-width: 959px)',
md: 'screen and (min-width: 960px) and (max-width: 1279px)',
lg: 'screen and (min-width: 1280px) and (max-width: 1919px)',
xl: 'screen and (min-width: 1920px) and (max-width: 5000px)',
lt-sm: 'screen and (max-width: 599px)',
lt-md: 'screen and (max-width: 959px)',
lt-lg: 'screen and (max-width: 1279px)',
lt-xl: 'screen and (max-width: 1919px)',
gt-xs: 'screen and (min-width: 600px)',
gt-sm: 'screen and (min-width: 960px)',
gt-md: 'screen and (min-width: 1280px)',
gt-lg: 'screen and (min-width: 1920px)'
) !default;
// Re-map the breakpoints for the helper classes
$helper-breakpoints: (
xs: null,
sm: 'gt-xs',
md: 'gt-sm',
lg: 'gt-md',
xl: 'gt-lg'
);
@mixin media-breakpoint($breakpointName) {
$mediaQuery: map-get($breakpoints, $breakpointName);
@if ($mediaQuery == null) {
@content
} @else {
@media #{$mediaQuery} {
@content
}
}
}

69
gui_wails03_learn/frontend/src/app/services/loader.service.ts

@ -0,0 +1,69 @@
import { Injectable, TemplateRef } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
export interface LoaderState {
isLoading: boolean;
show: boolean;
label: string | null;
templateRef: TemplateRef<any> | null;
templateName?: "default" | "card";
data?: {[key: string]: any};
}
export interface LoaderEvent {
label: string | null;
templateRef: TemplateRef<any> | null;
templateName?: "default" | "card";
data?: {[key: string]: any};
}
export interface LoaderEventBackend {
label: string | null;
templateName?: "default" | "card";
}
@Injectable({providedIn: 'root'})
export class LoaderService {
private _showSubject: BehaviorSubject<LoaderState> = new BehaviorSubject<LoaderState>({
isLoading: false,
show: false,
label: null,
templateRef: null,
templateName: 'default',
data: null
});
constructor() {
}
public get loaderState$(): Observable<LoaderState> {
return this._showSubject.asObservable();
}
public show(templateRef: TemplateRef<any> | string = null, data: any = null, templateName: 'default' | 'card' = 'card'): void {
this._showSubject.next({
isLoading: true,
show: true,
label: typeof templateRef === 'string' ? templateRef : null,
templateRef: typeof templateRef === 'object' ? templateRef : null,
templateName,
...data && { data }
});
}
public loading(): void {
this._showSubject.next({
isLoading: true,
show: false,
label: null,
templateRef: null,
});
}
public hide(): void {
this._showSubject.next({ isLoading: false, show: false, label: null, templateRef: null });
}
}

57
gui_wails03_learn/frontend/src/app/services/snackbar.service.ts

@ -0,0 +1,57 @@
import { LiveAnnouncer } from '@angular/cdk/a11y';
import { BreakpointObserver } from '@angular/cdk/layout';
import { Overlay } from '@angular/cdk/overlay';
import { Injectable, TemplateRef, EmbeddedViewRef, Injector, Optional, SkipSelf, Inject } from '@angular/core';
import { MatSnackBar, MatSnackBarRef, SimpleSnackBar, MatSnackBarConfig, MAT_SNACK_BAR_DEFAULT_OPTIONS } from '@angular/material/snack-bar';
import { TranslateService } from '@ngx-translate/core';
@Injectable({providedIn: 'root'})
export class SnackbarService extends MatSnackBar {
constructor(
private _trans: TranslateService,
_overlay: Overlay,
_live: LiveAnnouncer,
_injector: Injector,
_breakpointObserver: BreakpointObserver,
@Optional() @SkipSelf() _parentSnackBar: MatSnackBar,
@Inject(MAT_SNACK_BAR_DEFAULT_OPTIONS) _defaultConfig: MatSnackBarConfig,
) {
super(_overlay, _live, _injector, _breakpointObserver, _parentSnackBar, _defaultConfig)
}
public openSuccess(text: string, action: string = null, opts: MatSnackBarConfig = {}, transPayload: any = {}): MatSnackBarRef<SimpleSnackBar> {
const { panelClass, ...options } = opts || {};
const classes: string[] = [panelClass as string, 'success'];
return this.open(this._trans.instant(text, transPayload), action ? this._trans.instant(action) : null, { panelClass: classes, ...options });
}
public openError(text: string, action: string = null, opts: MatSnackBarConfig = {}, transPayload: any = {}): MatSnackBarRef<SimpleSnackBar> {
const { panelClass, ...options } = opts || {};
const classes: string[] = [panelClass as string, 'error'];
return this.open(this._trans.instant(text, transPayload), action ? this._trans.instant(action) : null, { panelClass: classes, ...options });
}
public openInfo(text: string, action: string = null, opts: MatSnackBarConfig = {}, transPayload: any = {}): MatSnackBarRef<SimpleSnackBar> {
const { panelClass, ...options } = opts || {};
const classes: string[] = [panelClass as string, 'info'];
return this.open(this._trans.instant(text, transPayload), action ? this._trans.instant(action) : null, { panelClass: classes, ...options });
}
public openWarning(text: string, action: string = null, opts: MatSnackBarConfig = {}, transPayload: any = {}): MatSnackBarRef<SimpleSnackBar> {
const { panelClass, ...options } = opts || {};
const classes: string[] = [panelClass as string, 'warning'];
return this.open(this._trans.instant(text, transPayload), action ? this._trans.instant(action) : null, { panelClass: classes, ...options });
}
public openTplSuccess(tpl: TemplateRef<any>, opts: MatSnackBarConfig = {}): MatSnackBarRef<EmbeddedViewRef<any>> {
const { panelClass, ...options } = opts;
const classes: string[] = [panelClass as string, 'success'];
return this.openFromTemplate(tpl, { panelClass: classes, ...options });
}
public openTplError(tpl: TemplateRef<any>, opts: MatSnackBarConfig = {}): MatSnackBarRef<EmbeddedViewRef<any>> {
const { panelClass, ...options } = opts;
const classes: string[] = [panelClass as string, 'error'];
return this.openFromTemplate(tpl, { panelClass: classes, ...options });
}
}

0
gui_wails03_learn/frontend/src/assets/.gitkeep

BIN
gui_wails03_learn/frontend/src/assets/MaterialIcons-Regular.eot

Binary file not shown.

1
gui_wails03_learn/frontend/src/assets/MaterialIcons-Regular.ijmap

File diff suppressed because one or more lines are too long

2373
gui_wails03_learn/frontend/src/assets/MaterialIcons-Regular.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 275 KiB

BIN
gui_wails03_learn/frontend/src/assets/MaterialIcons-Regular.ttf

Binary file not shown.

BIN
gui_wails03_learn/frontend/src/assets/MaterialIcons-Regular.woff

Binary file not shown.

BIN
gui_wails03_learn/frontend/src/assets/MaterialIcons-Regular.woff2

Binary file not shown.

BIN
gui_wails03_learn/frontend/src/assets/bg.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

184
gui_wails03_learn/frontend/src/assets/i18n/en.json

@ -0,0 +1,184 @@
{
"LANGUAGE": {
"EN": "English",
"PL": "Polish",
"IT": "Italian",
"ICON_DESC": "Change app language",
"SAVE_ERROR": "Cannot save language"
},
"HOME": {
"SEARCH": "Search",
"PASTE_TRACK_URL": "Paste here track url",
"CLOSE": "Close",
"NEW_UPDATE_AVAILABLE": "New update is available",
"REMOVE": {
"TRACK": "Remove track",
"PLAYLIST": "Remove playlist"
},
"REMOVED": {
"OK_TRACK": "Track has been removed",
"OK_PLAYLIST": "Playlist has been removed",
"KO_TRACK": "Cannot delete track",
"KO_PLAYLIST": "Cannot delete playlist"
},
"START_DOWNLOAD_TRACK": "Start download",
"STARTED_DOWNLOAD_TRACK": "Started downloading",
"CONVERT_TO_MP3_QUEUED": "Track added to queue for convert to mp3",
"WAITING_TO_CONVERT_MP3": "Waiting to be converted...",
"CONVERTING_MP3": "Converting to mp3...",
"NO_DOWNLOAED_TRACKS": "You have not downloaded tracks, start to download some",
"NO_SEARCH_RESULTS": "There are no tracks for your searching",
"OPEN_ON_YT": "Open on youtube",
"PLAYLISTS": {
"LABEL": "Playlists",
"CREATE": "Create playlist",
"CREATED_OK": "Playlist created",
"CREATED_KO": "Error while creating playlist",
"PLAYBACK": "Playback playlist",
"PAUSE": "Pause playlist",
"EXPORT": "Export",
"CANNOT_EXPORT_EMPTY": "Cannot export empty playlist",
"CHOOSE_EXPORT_DIRECTORY": "Choose directory",
"CHOOSE_EXPORT_DIRECTORY_KO": "Error while choosing export path: {{error}}",
"EXPORTED_OK": "Export has been ✅",
"EXPORTED_KO": "Something went wrong while exporting: {{error}}",
"REMOVE": "Remove playlist",
"ADD_TRACK": "Add to playlists",
"ALREADY_IN_PLAYLIST": "Track is already in this playlist",
"NO_PLAYLISTS": "No playlists, please create one",
"ADD_BTN": "Add",
"ADD_TRACK_OK": "Track has been added",
"ADD_TRACK_KO": "Error while adding track to playlist",
"LOADER": {
"EXPORTING": "Exporting..."
}
}
},
"PLAYLISTS": {
"LABEL": "Playlists",
"CREATE": "Create new playlist",
"PLAYBACK": "Playback playlist",
"PAUSE": "Pause playlist",
"EXPORT": "Export",
"REMOVE": "Remove playlist",
"ADD_TRACK": "Add to playlists",
"ALREADY_IN_PLAYLIST": "Track is already in this playlist",
"NO_PLAYLISTS": "No playlists, please create one",
"NAME": "Playlist name",
"PRESS_ENTER_HINT": "Press enter to create",
"ADD_BTN": "Add",
"CREATE_BTN": "Create playlist",
"CANNOT_PLAYBACK_EMPTY": "Cannot reproduce empty playlist",
"REMOVE_DIALOG": {
"TITLE": "Delete playlist",
"DESC": "Are you sure you would to remove <strong>{{name}}</strong> playlist?",
"KO": "Error while removing playlist",
"OK": "Playlist has been deleted"
},
"REMOVE_TRACK": {
"TITLE": "Delete playlist",
"DESC": "Are you sure you would to remove <strong>{{name}}</strong> playlist?",
"OK": "Track has been deleted from playlist",
"KO": "Error while removing track from playlist"
}
},
"PLAYER" : {
"PLAY": "Play",
"STOP": "Stop",
"PAUSE": "Pause",
"CANNOT_PLAY_PREV": "Cannot playback prev track",
"CANNOT_PLAY_NEXT": "Cannot playback next track"
},
"SETTINGS": {
"TITLE": "Settings",
"TABS": {
"GENERAL": "General",
"PUBLIC_SERVER": {
"LABEL": "Public server",
"DESC": "Share your tracks and playlists to others peoples",
"ENABLED": "On",
"DISABLED": "Off",
"NGROK": {
"NOT_INSTALLED": "You have to install ngrok tool on your system to enable this option",
"INSTALL_BTN": "Install ngrok",
"MOBILE_APP": "Download mobile app and scan your qr code",
"MOBILE_APP_BTN": "Download app",
"AUTH_TOKEN": "Your ngrok auth token",
"AUTH_TOKEN_HINT": "",
"AUTH_DESC_ENABLED": "Auth is enabled",
"AUTH_DESC_DISABLED": "Enable authentication for your public server",
"USERNAME": "Username",
"USERNAME_HINT": "Username used for auth",
"PASSWORD": "Password",
"PASSWORD_HINT": "Password used for auth",
"APIKEY_DESC_ENABLED": "Disable api key verification",
"APIKEY_DESC_DISABLED": "Enable api key verification",
"APP_KEY": "Verify api key (more secure)",
"APP_KEY_HINT": "Your local server will verify api key too",
"SHARE_URL": "Share url with your fiends",
"COPY_URL": "Copy to clipboard",
"URL_COPIED": "Url has been copied to your clipboard",
"ERROR_DETAILS_BTN": "Show details",
"CRASHED": "Ngrok service crashed unexpectedly",
"ON_STARTUP_ERRORS": {
"DEFAULT": "An error occurred: {{errCode}}",
"ERR_NGROK_103": "Your account seems to be suspended",
"ERR_NGROK_105": "Invalid authtoken",
"ERR_NGROK_107": "Invalid authtoken",
"ERR_NGROK_10888": "Max simultaneous ngrok clients reached, check if there is any ngrok process already running",
"ERR_NGROK_121": "You have an outdated ngrok version, please update it",
"ERR_NGROK_205": "Your account seems to be suspended",
"ERR_NGROK_225": "You have reached max simultaneous requests",
"ERR_NGROK_226": "You have reached max requests per interval"
}
}
}
},
"SAVE_DIRECTORY": "Save tracks to directory",
"SAVE_DIRECTORY_HINT": "Directory where all tracks will be saved",
"MAX_SIM_DOWNLOADS": "Max simultaneous downloads",
"MAX_SIM_DOWNLOADS_HINT": "Max number of tracks that will be downloaded simultaneously",
"CLIBOARD_WATCH": "Clipboard watch",
"DOWNLOAD_ON_COPY": "Download on copy",
"CONCURRENT_DOWNLOADS": "Concurrent downloads",
"CONCURRENT_PLAYLIST_DOWNLOADS": "Concurrent playlist downloads",
"CONVERT_TO_MP3": "Convert to mp3 in background",
"MISSING_FFMPEG": "You have to install ffmpeg tool on your system to enable this option",
"REMOVE_WEBM_AFTER_CONVERTING": "Remove webm files after converting to mp3 (disk space saving)",
"MUST_ENABLE_CONVERT_TO_MP3": "You have to enable \"Convert to mp3\" option before",
"TELEGRAM_SHARE": "Share downloaded tracks with your devices through telegram",
"TELEGRAM_USERNAME": "Insert your telegram username",
"TELEGRAM_ENABLE_USERNAME_LINK": "",
"TELEGRAM_USERNAMNE_EMPTY": "Cannot be empty if enabled",
"SAVE": "Save",
"SAVED": "Settings has been saved",
"SAVE_KO": "An error occured while saving settings",
"OPEN_DIALOG_BTN": "Open settings",
"MACOS_MISSING_CONVERT_TO_MP3_OPTIONS": "Cannot playback on MacOs, you should enable \"Convert to mp3\" option",
"MANUAL_MODE": {
"UNSUPPORTED_URL": "Unsupported url",
"TRACK_ADDED": "Track added to download",
"TRACK_ADD_KO": "Error while adding track to downloads"
}
},
"UPDATER": {
"TITLE": "New update is available",
"DESC": "New version is available <strong>{{version}}</strong> (you have {{oldVersion}}). <br/>",
"DOWNLOAD_MANUALLY_TITLE": "Download manually",
"DOWNLOAD_LINK": "Download & install manually <span class=\"open-link\">{{ name }}</span>",
"UPDATE_AND_RESTART_BTN": "Update & restart",
"UPDATE_BTN": "Update",
"LATER_BTN": "Later"
},
"FORMS": {
"BTNS": {
"CREATE": "Create",
"CANCEL": "Cancel",
"YES": "Yes",
"No": "No"
},
"VALIDATION": {
"EMPTY": "Cannot be empty"
}
}
}

184
gui_wails03_learn/frontend/src/assets/i18n/it.json

@ -0,0 +1,184 @@
{
"LANGUAGE": {
"EN": "Inglese",
"PL": "Polacco",
"IT": "Italiano",
"ICON_DESC": "Cambia lingua",
"SAVE_ERROR": "Errore durante il salvataggio della lingua"
},
"HOME": {
"SEARCH": "Cerca",
"PASTE_TRACK_URL": "Incolla l'url",
"CLOSE": "Chiudi",
"NEW_UPDATE_AVAILABLE": "Nuovo aggiornamento disponibile",
"REMOVE": {
"TRACK": "Elimina brano",
"PLAYLIST": "Elimina playlist"
},
"REMOVED": {
"OK_TRACK": "Il brano è stata eliminata",
"OK_PLAYLIST": "La playlist è stata eliminata",
"KO_TRACK": "Impossibile eliminare il brano",
"KO_PLAYLIST": "Impossibile eliminare la playlist"
},
"START_DOWNLOAD_TRACK": "Avvia il download",
"STARTED_DOWNLOAD_TRACK": "Il download è stato avviato",
"CONVERT_TO_MP3_QUEUED": "Il brano è in coda per la conversione",
"WAITING_TO_CONVERT_MP3": "In attesa per la conversione...",
"CONVERTING_MP3": "Conversione a mp3...",
"NO_DOWNLOAED_TRACKS": "Non hai scaricato ancora nulla, comincia ora",
"NO_SEARCH_RESULTS": "Non ci sono canzoni che soddisfano la ricerca",
"OPEN_ON_YT": "Apri su youtube",
"PLAYLISTS": {
"LABEL": "Playlists",
"CREATE": "Crea playlist",
"CREATED_OK": "Playlist creata",
"CREATED_KO": "Impossibile creare la playlist",
"PLAYBACK": "Riproduci playlist",
"PAUSE": "Pausa playlist",
"EXPORT": "Export",
"CANNOT_EXPORT_EMPTY": "Impossibile esportare una playlist vuota",
"CHOOSE_EXPORT_DIRECTORY": "Seleziona cartella",
"CHOOSE_EXPORT_DIRECTORY_KO": "Errore durante l'esportazione: {{error}}",
"EXPORTED_OK": "Export concluso ✅",
"EXPORTED_KO": "Qualcosa è andato storto durante l'esportazione: {{error}}",
"REMOVE": "Rimuovi playlist",
"ADD_TRACK": "Aggiungi alla playlist",
"ALREADY_IN_PLAYLIST": "Il brano risulta gia' presente nella playlist",
"NO_PLAYLISTS": "Non ci sono playlist, creane una",
"ADD_BTN": "Aggiungi",
"ADD_TRACK_OK": "La brano è stato aggiunto",
"ADD_TRACK_KO": "Errore durante l'aggiunta del brano alla playlist",
"LOADER": {
"EXPORTING": "Esportazione in corso..."
}
}
},
"PLAYLISTS": {
"LABEL": "PLe laylist",
"CREATE": "Crea una playlist",
"PLAYBACK": "Riproduci playlist",
"PAUSE": "Pause playlist",
"EXPORT": "Esporta",
"REMOVE": "Rimuovi playlist",
"ADD_TRACK": "Aggiungi alla playlists",
"ALREADY_IN_PLAYLIST": "Il brano risulta gia' presente nella playlist",
"NO_PLAYLISTS": "Non ci sono playlist, creane una",
"NAME": "Nome della playlist",
"PRESS_ENTER_HINT": "Premi enter per crearla",
"ADD_BTN": "Aggiungi",
"CREATE_BTN": "Crea playlist",
"CANNOT_PLAYBACK_EMPTY": "Impossibile ripridurre una playlist vuota",
"REMOVE_DIALOG": {
"TITLE": "Elimina playlist",
"DESC": "Sei sicuro di voler eliminare la playlist <strong>{{name}}</strong>?",
"KO": "Errore durante l'eliminazione della playlist",
"OK": "La playlist è stata eliminata"
},
"REMOVE_TRACK": {
"TITLE": "Delete playlist",
"DESC": "Are you sure you would to remove <strong>{{name}}</strong> playlist?",
"OK": "Il brano è stato eliminato dalla playlist",
"KO": "Impossibile rimuovere il brano dalla playlist"
}
},
"PLAYER" : {
"PLAY": "Play",
"STOP": "Stop",
"PAUSE": "Pause",
"CANNOT_PLAY_PREV": "Impossibile riprodurre la traccia precedente",
"CANNOT_PLAY_NEXT": "Impossibile riprodurre la prossima traccia"
},
"SETTINGS": {
"TITLE": "Impostazioni",
"SAVE_DIRECTORY": "Salva i brani in questa directory",
"SAVE_DIRECTORY_HINT": "Sara il percorso dove tutte i brani verranno salvati",
"MAX_SIM_DOWNLOADS": "N di download simultanei massimi",
"MAX_SIM_DOWNLOADS_HINT": "Numero massimo di brani scaricati contemporaneamente",
"CLIBOARD_WATCH": "Monitoraggio clipboard",
"DOWNLOAD_ON_COPY": "Avvia il download copiando un url (ctrl + c)",
"CONCURRENT_DOWNLOADS": "Download simultanei",
"CONCURRENT_PLAYLIST_DOWNLOADS": "Download simultanei delle playlist",
"CONVERT_TO_MP3": "Converti in mp3 in background",
"MISSING_FFMPEG": "Devi aver prima installato ffmpeg per abilitare questa opzione",
"REMOVE_WEBM_AFTER_CONVERTING": "Rimuovi i file originali (webm) al termine della conversione in mp3 (risparmio spazio)",
"MUST_ENABLE_CONVERT_TO_MP3": "Devi abilitare \"Converti in mp3\" per abilitare questa opzione",
"TELEGRAM_SHARE": "Condividi brani su instagram",
"TELEGRAM_USERNAME": "Inserisci il tuo nome utente di telegram",
"TELEGRAM_ENABLE_USERNAME_LINK": "",
"TELEGRAM_USERNAMNE_EMPTY": "Devi inserire un nome utente",
"SAVE": "Salva",
"SAVED": "Le impostazioni sono state salvate",
"SAVE_KO": "Impossibile salvare le impostazioni",
"OPEN_DIALOG_BTN": "Apri le impostazioni",
"MACOS_MISSING_CONVERT_TO_MP3_OPTIONS": "Impossibile riprodurre su Macos (abilita \"Converti in mp3\")",
"MANUAL_MODE": {
"UNSUPPORTED_URL": "Url non supportato",
"TRACK_ADDED": "Brano aggiunto al download",
"TRACK_ADD_KO": "Impossibile aggiungere il brano al download"
},
"TABS": {
"GENERAL": "Generali",
"PUBLIC_SERVER": {
"LABEL": "Condivisione",
"DESC": "Condividi i tuoi brani e le tue playlist con i tuoi amici",
"ENABLED": "Attiva",
"DISABLED": "Spenta",
"NGROK": {
"NOT_INSTALLED": "Devi prima installare ngrok per abilitare questa funzionalita",
"INSTALL_BTN": "Installa ngrok",
"MOBILE_APP": "Scarica l'app mobile e scanerizza il qrcode",
"MOBILE_APP_BTN": "Scarica l'app",
"AUTH_TOKEN": "Authtoken di ngrok",
"AUTH_TOKEN_HINT": "",
"AUTH_DESC_ENABLED": "L'autenticazione è abilitata",
"AUTH_DESC_DISABLED": "Abilita l'autenticazione (maggiore sicurezza)",
"USERNAME": "Username",
"USERNAME_HINT": "Username usata per l'autenticazione",
"PASSWORD": "Password",
"PASSWORD_HINT": "Password usata per l'autenticazione",
"APIKEY_DESC_ENABLED": "Disabilita la verifica della api key",
"APIKEY_DESC_DISABLED": "Abilita la verifica della api key",
"APP_KEY": "Verifica api key (maggiore sicurezza)",
"APP_KEY_HINT": "Verra' verificata la api key per la connessione",
"SHARE_URL": "Condividi l'url con i tuoi amici per accedere alle tue playlist",
"COPY_URL": "Copia nella clipboard",
"URL_COPIED": "L'url è stato copiato nei tuoi appunti",
"ERROR_DETAILS_BTN": "Mostra dettagli",
"CRASHED": "Il servizio ngrok è andato in crash inaspettatamente",
"ON_STARTUP_ERRORS": {
"DEFAULT": "Errore : {{errCode}}",
"ERR_NGROK_103": "Il tuo account risulta sospeso",
"ERR_NGROK_105": "Authtoken non valido",
"ERR_NGROK_107": "Authtoken non valido",
"ERR_NGROK_10888": "Risulta esserci gia in esecuzione ngrok controlla tra i processi",
"ERR_NGROK_121": "Hai una versione di ngrok obsoleta, per favore aggiornala",
"ERR_NGROK_205": "Il tuo account risulta sospeso",
"ERR_NGROK_225": "Hai raggiunto il numero massimo di richieste simultanee",
"ERR_NGROK_226": "Il numero massimo di richieste per intervallo è stato raggiunto"
}
}
}
}
},
"UPDATER": {
"TITLE": "Nuovo aggiornamento disponibile",
"DESC": "Nuovo aggiornamento disponibile <strong>{{version}}</strong> (attualmente usi {{oldVersion}}). <br/>",
"DOWNLOAD_MANUALLY_TITLE": "Scarica manualmente",
"DOWNLOAD_LINK": "Scarica & installa manualmente <span class=\"open-link\">{{ name }}</span>",
"UPDATE_AND_RESTART_BTN": "Aggiorna & riavvia",
"UPDATE_BTN": "Aggiorno",
"LATER_BTN": "Piu' tardi"
},
"FORMS": {
"BTNS": {
"CREATE": "Crea",
"CANCEL": "Annulla",
"YES": "Si",
"No": "No"
},
"VALIDATION": {
"EMPTY": "Campo obbligatorio"
}
}
}

184
gui_wails03_learn/frontend/src/assets/i18n/pl.json

@ -0,0 +1,184 @@
{
"LANGUAGE": {
"EN": "Angielski",
"PL": "Polski",
"IT": "Włoski",
"ICON_DESC": "Zmień język",
"SAVE_ERROR": "Bład nie mozna zmienić języka"
},
"HOME": {
"SEARCH": "Szukaj",
"PASTE_TRACK_URL": "Wklej tutaj link",
"CLOSE": "Zamknij",
"NEW_UPDATE_AVAILABLE": "Nowa aktualizacja jest dostępna",
"REMOVE": {
"TRACK": "Usuń utwór",
"PLAYLIST": "Usuń listę odtwarzania"
},
"REMOVED": {
"OK_TRACK": "Utwór został usunięty",
"OK_PLAYLIST": "Lista odtwarzania została usunięta",
"KO_TRACK": "Błąd nie można usunąć utworu",
"KO_PLAYLIST": "Błąd nie można usunąć listy odtwarzania"
},
"START_DOWNLOAD_TRACK": "Rozpocznij pobieranie",
"STARTED_DOWNLOAD_TRACK": "Pobieranie rozpoczete",
"CONVERT_TO_MP3_QUEUED": "Utwór dodany do kolejki do konwersji na mp3",
"WAITING_TO_CONVERT_MP3": "W kolejce do konwersji..",
"CONVERTING_MP3": "Trwa konwersja do mp3...",
"NO_DOWNLOAED_TRACKS": "Nie pobrałeś jeszcze zadnego utowru",
"NO_SEARCH_RESULTS": "Nie znaleziono zadnego utworu",
"OPEN_ON_YT": "Otwórz w youtube",
"PLAYLISTS": {
"LABEL": "Playlists",
"CREATE": "Utwórz playlistę",
"CREATED_OK": "Lista odtwarzania ",
"CREATED_KO": "Error while creating playlist",
"PLAYBACK": "Odtwórz playliste",
"PAUSE": "Przerwij playliste",
"EXPORT": "Eksportuj",
"CANNOT_EXPORT_EMPTY": "Nie mozna eksportowac pustej listy odtwarzania ",
"CHOOSE_EXPORT_DIRECTORY": "Wybierz sciezkę",
"CHOOSE_EXPORT_DIRECTORY_KO": "Błąd: {{error}}",
"EXPORTED_OK": "Eksport zakończony ✅",
"EXPORTED_KO": "Błąd przy eksportowaniu: {{error}}",
"REMOVE": "Usuń playlistę",
"ADD_TRACK": "Dodaj do playlisty",
"ALREADY_IN_PLAYLIST": "Utwór jest juz na playliście",
"NO_PLAYLISTS": "Nie masz list odtwarzania",
"ADD_BTN": "Dodaj",
"ADD_TRACK_OK": "Utwór został dodany",
"ADD_TRACK_KO": "Nie mozna dodać utworu",
"LOADER": {
"EXPORTING": "Eksportuje..."
}
}
},
"PLAYLISTS": {
"LABEL": "Playlisty",
"CREATE": "Stwórz playlistę",
"PLAYBACK": "Odtwórz playlistę",
"PAUSE": "Przerwij playlistę",
"EXPORT": "Eksportuj",
"REMOVE": "Usuń playlistę",
"ADD_TRACK": "Dodaj do playlisty",
"ALREADY_IN_PLAYLIST": "Utwór jest juz na playliście",
"NO_PLAYLISTS": "Nie masz list odtwarzania",
"NAME": "Nazwa playlisty",
"PRESS_ENTER_HINT": "Naciśnij Enter, aby utworzyć",
"ADD_BTN": "Dodaj",
"CREATE_BTN": "Playlista została stworzona",
"CANNOT_PLAYBACK_EMPTY": "Nie mozna odtworzyć pustej playlisty",
"REMOVE_DIALOG": {
"TITLE": "Usuń playlistę",
"DESC": "Czy na pewno chcesz usunąć utwór z <strong>{{name}}</strong> playlisty?",
"KO": "Bład przy usuwaniu playlisty",
"OK": "Playlista została usunięta"
},
"REMOVE_TRACK": {
"TITLE": "Usuń playlistę",
"DESC": "Are you sure you would to remove <strong>{{name}}</strong> playlist?",
"OK": "Utwór został usunięty",
"KO": "Błąd przy usuwaniu utwóru"
}
},
"PLAYER" : {
"PLAY": "Odtwórz",
"STOP": "Stop",
"PAUSE": "Pauza",
"CANNOT_PLAY_PREV": "Nie można odtworzyć poprzedniego utworu",
"CANNOT_PLAY_NEXT": "Nie można odtworzyć następnego utworu"
},
"SETTINGS": {
"TITLE": "Ustawienia",
"SAVE_DIRECTORY": "Wybierz ścięzkę",
"SAVE_DIRECTORY_HINT": "Ściezka gdzie bedą zapisawane utwory",
"MAX_SIM_DOWNLOADS": "Liczba jednocześnych pobrań",
"MAX_SIM_DOWNLOADS_HINT": "Maksymalna liczba utworów, które zostaną pobrane jednocześnie",
"CLIBOARD_WATCH": "Nasłuchuj schwowek",
"DOWNLOAD_ON_COPY": "Pobierz przy kopiowaniu",
"CONCURRENT_DOWNLOADS": "Współbieżne pobieranie utworów",
"CONCURRENT_PLAYLIST_DOWNLOADS": "Współbieżne pobieranie playlist",
"CONVERT_TO_MP3": "Konwertuj do mp3 w tle",
"MISSING_FFMPEG": "Musisz najpierw zainstalować ffmpeg aby włączyć tę opcję",
"REMOVE_WEBM_AFTER_CONVERTING": "Usuń originalne pliki webm po konwersji (oszczędza miejsce na dysku)",
"MUST_ENABLE_CONVERT_TO_MP3": "Musisz wcześniej włączyć opcję \"Konwertuj na mp3\"",
"TELEGRAM_SHARE": "Udostępniaj utwory przez telegram",
"TELEGRAM_USERNAME": "Wprowadź swoją nazwę użytkownika telegramu",
"TELEGRAM_ENABLE_USERNAME_LINK": "",
"TELEGRAM_USERNAMNE_EMPTY": "Pole nie moze by puste jeśli opcja jest włączona",
"SAVE": "Zapisz",
"SAVED": "Zapisano",
"SAVE_KO": "Błąd przy zapisywaniu ustawień",
"OPEN_DIALOG_BTN": "Ustawienia",
"MACOS_MISSING_CONVERT_TO_MP3_OPTIONS": "Bład przy odtwarzaniu na MacOs, należy włączyć opcję \"Konwertuj na mp3\"",
"MANUAL_MODE": {
"UNSUPPORTED_URL": "Nieobsługiwany adres URL",
"TRACK_ADDED": "Utwór dodany do pobrania",
"TRACK_ADD_KO": "Bład przy dodawaniu utworu do listy pobierania"
},
"TABS": {
"GENERAL": "Ogólne",
"PUBLIC_SERVER": {
"LABEL": "Tunelowanie",
"DESC": "Udostepniaj twoim znajomym twoje utwory i listy odtwarzania",
"ENABLED": "Aktywne",
"DISABLED": "Nieaktywne",
"NGROK": {
"NOT_INSTALLED": "Musisz najpierw zainstalować ngrok, aby włączyć tę funkcję",
"INSTALL_BTN": "Zainstaluj ngrok",
"MOBILE_APP": "Ściągnij aplikacje i zeskanuj qrcode",
"MOBILE_APP_BTN": "Ściągnij aplikacje",
"AUTH_TOKEN": "Token ngrok",
"AUTH_TOKEN_HINT": "",
"AUTH_DESC_ENABLED": "Autentykacja włączona",
"AUTH_DESC_DISABLED": "Włącz uwierzytelnianie (zwiększone bezpieczeństwo)",
"USERNAME": "Użytkownik",
"USERNAME_HINT": "Nazwa użytkownik potrzebna do autentykacji",
"PASSWORD": "Hasło",
"PASSWORD_HINT": "Hasło użytkownika potrzebne do autentykacji",
"APIKEY_DESC_ENABLED": "Wyłącz weryfikację klucza API",
"APIKEY_DESC_DISABLED": "Włącz weryfikację klucza API",
"APP_KEY": "Zweryfikuj klucz API (większe bezpieczeństwo)",
"APP_KEY_HINT": "Aby połączyc sie trzeba bedzie podac klucz API",
"SHARE_URL": "Udostępnij adres URL znajomym, aby uzyskać dostęp do twoich list odtwarzania",
"COPY_URL": "Skopiuj do schowka",
"URL_COPIED": "Adres URL został skopiowany do schowka",
"ERROR_DETAILS_BTN": "Pokaż szczegóły",
"CRASHED": "Ngrok niespodziewanie się zawiesił",
"ON_STARTUP_ERRORS": {
"DEFAULT": "Wystąpił błąd: {{errCode}}",
"ERR_NGROK_103": "Wygląda na to, że Twoje konto zostało zawieszone",
"ERR_NGROK_105": "Nieprawidłowy token uwierzytelniający",
"ERR_NGROK_107": "Nieprawidłowy token uwierzytelniający",
"ERR_NGROK_10888": "Wygląda na to, że ngrok już działa w tle, sprawdź czy jest uruchomiony jakiś proces ngrok w tle",
"ERR_NGROK_121": "Masz przestarzałą wersję ngrok, zaktualizuj ją",
"ERR_NGROK_205": "Wygląda na to, że Twoje konto zostało zawieszone",
"ERR_NGROK_225": "Osiągnięto maksymalną liczbę jednoczesnych żądań",
"ERR_NGROK_226": "Osiągnięto maksymalną liczbę jednoczesnych żądań"
}
}
}
}
},
"UPDATER": {
"TITLE": "Nowa aktualizacja jest dostępna",
"DESC": "Nowa aktualizacja jest dostępna <strong>{{version}}</strong> (uzywasz {{oldVersion}}). <br/>",
"DOWNLOAD_MANUALLY_TITLE": "Download manually",
"DOWNLOAD_LINK": "Ściągnij & aktualizuj ręcznie <span class=\"open-link\">{{ name }}</span>",
"UPDATE_AND_RESTART_BTN": "Aktualizuj & uruchom ponownie",
"UPDATE_BTN": "Aktualizuj",
"LATER_BTN": "Pózniej"
},
"FORMS": {
"BTNS": {
"CREATE": "Stwórz",
"CANCEL": "Anuluj",
"YES": "Tak",
"No": "Nie"
},
"VALIDATION": {
"EMPTY": "Pole obowiązkowe"
}
}
}

3
gui_wails03_learn/frontend/src/assets/icons/download-failed.svg

@ -0,0 +1,3 @@
<svg style="width:24px;height:24px" viewBox="0 0 24 24">
<path fill="currentColor" d="M20.84 22.73L18.11 20H5V18H16.11L13.06 14.95L12 16L5 9H7.11L1.11 3L2.39 1.73L22.11 21.46L20.84 22.73M19 9H15V3H9V5.8L15.6 12.4L19 9Z" />
</svg>

After

Width:  |  Height:  |  Size: 234 B

3
gui_wails03_learn/frontend/src/assets/icons/github.svg

@ -0,0 +1,3 @@
<svg style="width:24px;height:24px" viewBox="0 0 24 24">
<path fill="#fffff" d="M12,2A10,10 0 0,0 2,12C2,16.42 4.87,20.17 8.84,21.5C9.34,21.58 9.5,21.27 9.5,21C9.5,20.77 9.5,20.14 9.5,19.31C6.73,19.91 6.14,17.97 6.14,17.97C5.68,16.81 5.03,16.5 5.03,16.5C4.12,15.88 5.1,15.9 5.1,15.9C6.1,15.97 6.63,16.93 6.63,16.93C7.5,18.45 8.97,18 9.54,17.76C9.63,17.11 9.89,16.67 10.17,16.42C7.95,16.17 5.62,15.31 5.62,11.5C5.62,10.39 6,9.5 6.65,8.79C6.55,8.54 6.2,7.5 6.75,6.15C6.75,6.15 7.59,5.88 9.5,7.17C10.29,6.95 11.15,6.84 12,6.84C12.85,6.84 13.71,6.95 14.5,7.17C16.41,5.88 17.25,6.15 17.25,6.15C17.8,7.5 17.45,8.54 17.35,8.79C18,9.5 18.38,10.39 18.38,11.5C18.38,15.32 16.04,16.16 13.81,16.41C14.17,16.72 14.5,17.33 14.5,18.26C14.5,19.6 14.5,20.68 14.5,21C14.5,21.27 14.66,21.59 15.17,21.5C19.14,20.16 22,16.42 22,12A10,10 0 0,0 12,2Z" />
</svg>

After

Width:  |  Height:  |  Size: 843 B

3
gui_wails03_learn/frontend/src/assets/icons/qrcode-scan.svg

@ -0,0 +1,3 @@
<svg style="width:24px;height:24px" viewBox="0 0 24 24">
<path fill="currentColor" d="M4,4H10V10H4V4M20,4V10H14V4H20M14,15H16V13H14V11H16V13H18V11H20V13H18V15H20V18H18V20H16V18H13V20H11V16H14V15M16,15V18H18V15H16M4,20V14H10V20H4M6,6V8H8V6H6M16,6V8H18V6H16M6,16V18H8V16H6M4,11H6V13H4V11M9,11H13V15H11V13H9V11M11,6H13V10H11V6M2,2V6H0V2A2,2 0 0,1 2,0H6V2H2M22,0A2,2 0 0,1 24,2V6H22V2H18V0H22M2,18V22H6V24H2A2,2 0 0,1 0,22V18H2M22,22V18H24V22A2,2 0 0,1 22,24H18V22H22Z" />
</svg>

After

Width:  |  Height:  |  Size: 480 B

36
gui_wails03_learn/frontend/src/assets/material-icons.css

@ -0,0 +1,36 @@
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: url(http://localhost:8080/static/frontend/dist/assets/MaterialIcons-Regular.eot); /* For IE6-8 */
src: local('Material Icons'),
local('MaterialIcons-Regular'),
url(http://localhost:8080/static/frontend/dist/assets/MaterialIcons-Regular.woff2) format('woff2'),
url(http://localhost:8080/static/frontend/dist/assets/MaterialIcons-Regular.woff) format('woff'),
url(http://localhost:8080/static/frontend/dist/assets/MaterialIcons-Regular.ttf) format('truetype');
}
.material-icons {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px; /* Preferred icon size */
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
/* Support for all WebKit browsers. */
-webkit-font-smoothing: antialiased;
/* Support for Safari and Chrome. */
text-rendering: optimizeLegibility;
/* Support for Firefox. */
-moz-osx-font-smoothing: grayscale;
/* Support for IE. */
font-feature-settings: 'liga';
}

BIN
gui_wails03_learn/frontend/src/assets/playing-wave.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

387
gui_wails03_learn/frontend/src/backend/index.js

@ -0,0 +1,387 @@
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
const backend = {
"main": {
"AppState": {
/**
* AddToDownload
* @param {string} arg1 - Go Type: string
* @param {boolean} arg2 - Go Type: bool
* @returns {Promise<Error>} - Go Type: error
*/
"AddToDownload": (arg1, arg2) => {
return window.backend.main.AppState.AddToDownload(arg1, arg2);
},
/**
* Deadline
* @returns {Promise<any|boolean>} - Go Type: time.Time
*/
"Deadline": () => {
return window.backend.main.AppState.Deadline();
},
/**
* Done
* @returns {Promise<any>} - Go Type: <-chan struct {}
*/
"Done": () => {
return window.backend.main.AppState.Done();
},
/**
* Err
* @returns {Promise<Error>} - Go Type: error
*/
"Err": () => {
return window.backend.main.AppState.Err();
},
/**
* ForceQuit
* @returns {Promise<void>}
*/
"ForceQuit": () => {
return window.backend.main.AppState.ForceQuit();
},
/**
* GetAppConfig
* @returns {Promise<any>} - Go Type: *models.AppConfig
*/
"GetAppConfig": () => {
return window.backend.main.AppState.GetAppConfig();
},
/**
* GetEntryById
* @param {any} arg1 - Go Type: models.GenericEntry
* @returns {Promise<any>} - Go Type: *models.GenericEntry
*/
"GetEntryById": (arg1) => {
return window.backend.main.AppState.GetEntryById(arg1);
},
/**
* InitializeListeners
* @returns {Promise<void>}
*/
"InitializeListeners": () => {
return window.backend.main.AppState.InitializeListeners();
},
/**
* IsFFmpegInstalled
* @returns {Promise<string|Error>} - Go Type: string
*/
"IsFFmpegInstalled": () => {
return window.backend.main.AppState.IsFFmpegInstalled();
},
/**
* IsSupportedUrl
* @param {string} arg1 - Go Type: string
* @returns {Promise<boolean>} - Go Type: bool
*/
"IsSupportedUrl": (arg1) => {
return window.backend.main.AppState.IsSupportedUrl(arg1);
},
/**
* OpenUrl
* @param {string} arg1 - Go Type: string
* @returns {Promise<Error>} - Go Type: error
*/
"OpenUrl": (arg1) => {
return window.backend.main.AppState.OpenUrl(arg1);
},
/**
* PreWailsInit
* @param {any} arg1 - Go Type: context.Context
* @returns {Promise<void>}
*/
"PreWailsInit": (arg1) => {
return window.backend.main.AppState.PreWailsInit(arg1);
},
/**
* ReadSettingBoolValue
* @param {string} arg1 - Go Type: string
* @returns {Promise<boolean|Error>} - Go Type: bool
*/
"ReadSettingBoolValue": (arg1) => {
return window.backend.main.AppState.ReadSettingBoolValue(arg1);
},
/**
* ReadSettingValue
* @param {string} arg1 - Go Type: string
* @returns {Promise<string|Error>} - Go Type: string
*/
"ReadSettingValue": (arg1) => {
return window.backend.main.AppState.ReadSettingValue(arg1);
},
/**
* ReloadNewLanguage
* @returns {Promise<void>}
*/
"ReloadNewLanguage": () => {
return window.backend.main.AppState.ReloadNewLanguage();
},
/**
* RemoveEntry
* @param {any} arg1 - Go Type: map[string]interface {}
* @returns {Promise<Error>} - Go Type: error
*/
"RemoveEntry": (arg1) => {
return window.backend.main.AppState.RemoveEntry(arg1);
},
/**
* SaveSettingBoolValue
* @param {string} arg1 - Go Type: string
* @param {boolean} arg2 - Go Type: bool
* @returns {Promise<Error>} - Go Type: error
*/
"SaveSettingBoolValue": (arg1, arg2) => {
return window.backend.main.AppState.SaveSettingBoolValue(arg1, arg2);
},
/**
* SaveSettingValue
* @param {string} arg1 - Go Type: string
* @param {string} arg2 - Go Type: string
* @returns {Promise<Error>} - Go Type: error
*/
"SaveSettingValue": (arg1, arg2) => {
return window.backend.main.AppState.SaveSettingValue(arg1, arg2);
},
/**
* SelectDirectory
* @returns {Promise<string|Error>} - Go Type: string
*/
"SelectDirectory": () => {
return window.backend.main.AppState.SelectDirectory();
},
/**
* ShowWindow
* @returns {Promise<void>}
*/
"ShowWindow": () => {
return window.backend.main.AppState.ShowWindow();
},
/**
* StartDownload
* @param {any} arg1 - Go Type: map[string]interface {}
* @returns {Promise<Error>} - Go Type: error
*/
"StartDownload": (arg1) => {
return window.backend.main.AppState.StartDownload(arg1);
},
/**
* Update
* @param {boolean} arg1 - Go Type: bool
* @returns {Promise<void>}
*/
"Update": (arg1) => {
return window.backend.main.AppState.Update(arg1);
},
/**
* Value
* @param {number} arg1 - Go Type: interface {}
* @returns {Promise<number>} - Go Type: interface {}
*/
"Value": (arg1) => {
return window.backend.main.AppState.Value(arg1);
},
}
"NgrokService": {
/**
* Deadline
* @returns {Promise<any|boolean>} - Go Type: time.Time
*/
"Deadline": () => {
return window.backend.main.NgrokService.Deadline();
},
/**
* Done
* @returns {Promise<any>} - Go Type: <-chan struct {}
*/
"Done": () => {
return window.backend.main.NgrokService.Done();
},
/**
* Err
* @returns {Promise<Error>} - Go Type: error
*/
"Err": () => {
return window.backend.main.NgrokService.Err();
},
/**
* ExitCode
* @returns {Promise<number>} - Go Type: int
*/
"ExitCode": () => {
return window.backend.main.NgrokService.ExitCode();
},
/**
* Exited
* @returns {Promise<boolean>} - Go Type: bool
*/
"Exited": () => {
return window.backend.main.NgrokService.Exited();
},
/**
* GetPublicUrl
* @param {any} arg1 - Go Type: chan main.NgrokTunnelInfo
* @returns {Promise<void>}
*/
"GetPublicUrl": (arg1) => {
return window.backend.main.NgrokService.GetPublicUrl(arg1);
},
/**
* IsNgrokInstalled
* @returns {Promise<string|Error>} - Go Type: string
*/
"IsNgrokInstalled": () => {
return window.backend.main.NgrokService.IsNgrokInstalled();
},
/**
* KillProcess
* @returns {Promise<Error>} - Go Type: error
*/
"KillProcess": () => {
return window.backend.main.NgrokService.KillProcess();
},
/**
* MonitorNgrokProcess
* @returns {Promise<void>}
*/
"MonitorNgrokProcess": () => {
return window.backend.main.NgrokService.MonitorNgrokProcess();
},
/**
* Pid
* @returns {Promise<number>} - Go Type: int
*/
"Pid": () => {
return window.backend.main.NgrokService.Pid();
},
/**
* SetAuthToken
* @returns {Promise<Error>} - Go Type: error
*/
"SetAuthToken": () => {
return window.backend.main.NgrokService.SetAuthToken();
},
/**
* StartProcess
* @param {boolean} arg1 - Go Type: bool
* @returns {Promise<any>} - Go Type: main.NgrokProcessResult
*/
"StartProcess": (arg1) => {
return window.backend.main.NgrokService.StartProcess(arg1);
},
/**
* String
* @returns {Promise<string>} - Go Type: string
*/
"String": () => {
return window.backend.main.NgrokService.String();
},
/**
* Success
* @returns {Promise<boolean>} - Go Type: bool
*/
"Success": () => {
return window.backend.main.NgrokService.Success();
},
/**
* Sys
* @returns {Promise<number>} - Go Type: interface {}
*/
"Sys": () => {
return window.backend.main.NgrokService.Sys();
},
/**
* SysUsage
* @returns {Promise<number>} - Go Type: interface {}
*/
"SysUsage": () => {
return window.backend.main.NgrokService.SysUsage();
},
/**
* SystemTime
* @returns {Promise<any>} - Go Type: time.Duration
*/
"SystemTime": () => {
return window.backend.main.NgrokService.SystemTime();
},
/**
* UserTime
* @returns {Promise<any>} - Go Type: time.Duration
*/
"UserTime": () => {
return window.backend.main.NgrokService.UserTime();
},
/**
* Value
* @param {number} arg1 - Go Type: interface {}
* @returns {Promise<number>} - Go Type: interface {}
*/
"Value": (arg1) => {
return window.backend.main.NgrokService.Value(arg1);
},
}
"OfflinePlaylistService": {
/**
* AddTrackToPlaylist
* @param {Array.<any>} arg1 - Go Type: []map[string]interface {}
* @returns {Promise<boolean|Error>} - Go Type: bool
*/
"AddTrackToPlaylist": (arg1) => {
return window.backend.main.OfflinePlaylistService.AddTrackToPlaylist(arg1);
},
/**
* CreateNewPlaylist
* @param {string} arg1 - Go Type: string
* @returns {Promise<any|Error>} - Go Type: models.OfflinePlaylist
*/
"CreateNewPlaylist": (arg1) => {
return window.backend.main.OfflinePlaylistService.CreateNewPlaylist(arg1);
},
/**
* ExportPlaylist
* @param {string} arg1 - Go Type: string
* @param {string} arg2 - Go Type: string
* @returns {Promise<boolean|Error>} - Go Type: bool
*/
"ExportPlaylist": (arg1, arg2) => {
return window.backend.main.OfflinePlaylistService.ExportPlaylist(arg1, arg2);
},
/**
* GetPlaylistByUUID
* @param {string} arg1 - Go Type: string
* @returns {Promise<any>} - Go Type: models.OfflinePlaylist
*/
"GetPlaylistByUUID": (arg1) => {
return window.backend.main.OfflinePlaylistService.GetPlaylistByUUID(arg1);
},
/**
* GetPlaylists
* @param {boolean} arg1 - Go Type: bool
* @returns {Promise<Array.<any>|Error>} - Go Type: []models.OfflinePlaylist
*/
"GetPlaylists": (arg1) => {
return window.backend.main.OfflinePlaylistService.GetPlaylists(arg1);
},
/**
* RemovePlaylist
* @param {string} arg1 - Go Type: string
* @returns {Promise<boolean|Error>} - Go Type: bool
*/
"RemovePlaylist": (arg1) => {
return window.backend.main.OfflinePlaylistService.RemovePlaylist(arg1);
},
/**
* RemoveTrackFromPlaylist
* @param {string} arg1 - Go Type: string
* @param {any} arg2 - Go Type: models.OfflinePlaylist
* @returns {Promise<any|Error>} - Go Type: models.OfflinePlaylist
*/
"RemoveTrackFromPlaylist": (arg1, arg2) => {
return window.backend.main.OfflinePlaylistService.RemoveTrackFromPlaylist(arg1, arg2);
},
}
}
};
export default backend;

11
gui_wails03_learn/frontend/src/backend/package.json

@ -0,0 +1,11 @@
{
"name": "backend",
"version": "1.0.0",
"description": "Package to wrap backend method calls",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}

3
gui_wails03_learn/frontend/src/environments/environment.hmr.ts

@ -0,0 +1,3 @@
export const environment = {
production: false
};

3
gui_wails03_learn/frontend/src/environments/environment.prod.ts

@ -0,0 +1,3 @@
export const environment = {
production: true
};

16
gui_wails03_learn/frontend/src/environments/environment.ts

@ -0,0 +1,16 @@
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.

BIN
gui_wails03_learn/frontend/src/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 948 B

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save