@ -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 |