@ -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 |
@ -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/ |
@ -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. |
@ -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...) |
@ -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 |
|||
} |
After Width: | Height: | Size: 106 KiB |
@ -0,0 +1,4 @@ |
|||
# go build -ldflags="-s -w -H=windowsgui" main.go |
|||
|
|||
cd frontend |
|||
npm install && npm run build && cd ../ && wails build |
@ -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) |
|||
} |
|||
} |
@ -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() |
|||
} |
@ -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() |
|||
} |
@ -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 |
|||
} |
@ -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" |
|||
) |
@ -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) |
|||
} |
@ -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 |
|||
} |
@ -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. |
@ -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 |
@ -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 |
@ -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. |
@ -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" |
|||
} |
@ -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 |
|||
}); |
|||
}; |
@ -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" |
|||
} |
|||
} |
@ -0,0 +1 @@ |
|||
68adfb04b171d5f769382862f9eb5972 |
@ -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 { } |
@ -0,0 +1,3 @@ |
|||
<router-outlet></router-outlet> |
|||
<audio-player></audio-player> |
|||
<loader-overlay></loader-overlay> |
@ -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!'); |
|||
}); |
|||
}); |
@ -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) |
|||
}) |
|||
} |
|||
} |
@ -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 { } |
@ -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') |
|||
) |
|||
} |
@ -0,0 +1,3 @@ |
|||
export const minMax = (min: number, max: number) => { |
|||
return Math.floor(Math.random() * (max - min)) + min; |
|||
} |
@ -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> |
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
@ -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; |
|||
} |
|||
} |
@ -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'; |
|||
}); |
|||
} |
|||
} |
@ -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> |
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
@ -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); |
|||
} |
|||
|
|||
} |
@ -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'; |
@ -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> |
@ -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; |
|||
} |
|||
} |
|||
} |
|||
|
@ -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(); |
|||
}) |
|||
} |
|||
} |
@ -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> |
@ -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); |
|||
} |
|||
} |
@ -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(); |
|||
} |
|||
} |
@ -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> |
@ -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; |
|||
} |
|||
} |
|||
} |
@ -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(); |
|||
} |
|||
} |
@ -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> |
@ -0,0 +1,10 @@ |
|||
create-playlist { |
|||
[mat-dialog-content] { |
|||
padding: 16px 16px 0px !important; |
|||
margin: 0; |
|||
|
|||
.mat-form-field { |
|||
width: 100%; |
|||
} |
|||
} |
|||
} |
@ -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(); |
|||
} |
|||
} |
@ -0,0 +1,2 @@ |
|||
export * from './add-to-playlist.component'; |
|||
export * from './create.component'; |
@ -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> |
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
@ -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 "{}"; |
|||
} |
|||
} |
@ -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> |
@ -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; |
|||
} |
|||
} |
|||
} |
@ -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(); |
|||
} |
|||
} |
@ -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(); |
|||
} |
|||
|
|||
} |
@ -0,0 +1 @@ |
|||
export * from './autofocus.directive'; |
@ -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>; |
|||
} |
|||
} |
|||
} |
@ -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'; |
|||
} |
@ -0,0 +1,6 @@ |
|||
export * from './app-state'; |
|||
export * from './entry'; |
|||
export * from './playlist'; |
|||
export * from './track'; |
|||
export * from './update-release'; |
|||
export * from './ngrok'; |
@ -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(); |
|||
} |
|||
|
@ -0,0 +1,5 @@ |
|||
export interface OfflinePlaylist { |
|||
uuid: string; |
|||
name: string; |
|||
tracksIds: string[]; |
|||
} |
@ -0,0 +1,8 @@ |
|||
export interface Playlist { |
|||
id: string; |
|||
count: number; |
|||
name: string; |
|||
thumbnail: string; |
|||
tracksIds: string[]; |
|||
url: string |
|||
} |
@ -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; |
|||
} |
@ -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) |
|||
} |
|||
} |
@ -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> |
@ -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%); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
@ -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); |
|||
} |
|||
} |
@ -0,0 +1,2 @@ |
|||
export * from './home/home.component'; |
|||
export * from './offline-playlist/offline-playlist.component'; |
@ -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> |
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
@ -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(); |
|||
} |
|||
} |
@ -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 |
|||
} |
|||
} |
|||
} |
@ -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 }); |
|||
} |
|||
|
|||
} |
@ -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 }); |
|||
} |
|||
} |
After Width: | Height: | Size: 275 KiB |
After Width: | Height: | Size: 318 KiB |
@ -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" |
|||
} |
|||
} |
|||
} |
@ -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" |
|||
} |
|||
} |
|||
} |
@ -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" |
|||
} |
|||
} |
|||
} |
After Width: | Height: | Size: 234 B |
After Width: | Height: | Size: 843 B |
After Width: | Height: | Size: 480 B |
@ -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'; |
|||
} |
After Width: | Height: | Size: 24 KiB |
@ -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; |
@ -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" |
|||
} |
@ -0,0 +1,3 @@ |
|||
export const environment = { |
|||
production: false |
|||
}; |
@ -0,0 +1,3 @@ |
|||
export const environment = { |
|||
production: true |
|||
}; |
@ -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.
|
After Width: | Height: | Size: 948 B |