mirror of
https://github.com/anotherhadi/proton-vpn-tui.git
synced 2026-10-05 11:18:27 +02:00
319 lines
7.8 KiB
Go
319 lines
7.8 KiB
Go
package app
|
|
|
|
import (
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/anotherhadi/proton-vpn-tui/internal/backend"
|
|
)
|
|
|
|
type rowKind int
|
|
|
|
const (
|
|
rowCountry rowKind = iota
|
|
rowCity
|
|
rowServer
|
|
)
|
|
|
|
type row struct {
|
|
kind rowKind
|
|
|
|
country string
|
|
city string
|
|
server backend.LogicalServer
|
|
|
|
expanded bool
|
|
favorite bool
|
|
|
|
serverCount int
|
|
avgLoad int
|
|
}
|
|
|
|
func (r row) FilterValue() string {
|
|
switch r.kind {
|
|
case rowServer:
|
|
return r.server.Name + " " + r.city + " " + backend.CountryName(r.country)
|
|
case rowCity:
|
|
return r.city + " " + backend.CountryName(r.country)
|
|
default:
|
|
return r.country + " " + backend.CountryName(r.country)
|
|
}
|
|
}
|
|
|
|
func cityKey(country, city string) string { return country + "\x00" + city }
|
|
|
|
type countryGroup struct {
|
|
code string
|
|
cities map[string][]backend.LogicalServer
|
|
cityOrder []string
|
|
servers []backend.LogicalServer
|
|
}
|
|
|
|
func buildTree(servers []backend.LogicalServer, f filters) []*countryGroup {
|
|
groups := make(map[string]*countryGroup)
|
|
var order []string
|
|
|
|
for _, s := range servers {
|
|
if s.Tor != f.Tor || s.SecureCore != f.SecureCore {
|
|
continue
|
|
}
|
|
if f.P2P && !s.P2P {
|
|
continue
|
|
}
|
|
if f.Free && !s.IsFree() {
|
|
continue
|
|
}
|
|
|
|
code := s.EntryCountry
|
|
if s.SecureCore {
|
|
code = s.ExitCountry
|
|
}
|
|
|
|
g, ok := groups[code]
|
|
if !ok {
|
|
g = &countryGroup{code: code, cities: make(map[string][]backend.LogicalServer)}
|
|
groups[code] = g
|
|
order = append(order, code)
|
|
}
|
|
|
|
city := ""
|
|
if s.City != nil {
|
|
city = *s.City
|
|
}
|
|
if _, ok := g.cities[city]; !ok {
|
|
g.cityOrder = append(g.cityOrder, city)
|
|
}
|
|
g.cities[city] = append(g.cities[city], s)
|
|
g.servers = append(g.servers, s)
|
|
}
|
|
|
|
result := make([]*countryGroup, 0, len(order))
|
|
for _, code := range order {
|
|
result = append(result, groups[code])
|
|
}
|
|
sort.Slice(result, func(i, j int) bool {
|
|
return backend.CountryName(result[i].code) < backend.CountryName(result[j].code)
|
|
})
|
|
for _, g := range result {
|
|
sort.Slice(g.cityOrder, func(i, j int) bool {
|
|
a, b := g.cityOrder[i], g.cityOrder[j]
|
|
if a == "" {
|
|
return false
|
|
}
|
|
if b == "" {
|
|
return true
|
|
}
|
|
return a < b
|
|
})
|
|
for _, servers := range g.cities {
|
|
sort.Slice(servers, func(i, j int) bool { return servers[i].Name < servers[j].Name })
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func avgLoad(servers []backend.LogicalServer) int {
|
|
if len(servers) == 0 {
|
|
return 0
|
|
}
|
|
total := 0
|
|
for _, s := range servers {
|
|
total += s.Load
|
|
}
|
|
return total / len(servers)
|
|
}
|
|
|
|
func serverIsFavorite(s backend.LogicalServer, favServers map[string]bool) bool {
|
|
return favServers[strings.ToUpper(s.Name)]
|
|
}
|
|
|
|
func serversHaveFavorite(servers []backend.LogicalServer, favServers map[string]bool) bool {
|
|
for _, s := range servers {
|
|
if serverIsFavorite(s, favServers) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func countryIsFavorite(g *countryGroup, favCountries, favServers map[string]bool) bool {
|
|
return favCountries[g.code] || serversHaveFavorite(g.servers, favServers)
|
|
}
|
|
|
|
func orderWithFavoritesFirst(tree []*countryGroup, favCountries, favServers map[string]bool) []*countryGroup {
|
|
ordered := append([]*countryGroup(nil), tree...)
|
|
sort.SliceStable(ordered, func(i, j int) bool {
|
|
return countryIsFavorite(ordered[i], favCountries, favServers) &&
|
|
!countryIsFavorite(ordered[j], favCountries, favServers)
|
|
})
|
|
return ordered
|
|
}
|
|
|
|
func orderCitiesWithFavoritesFirst(g *countryGroup, favServers map[string]bool) []string {
|
|
cities := append([]string(nil), g.cityOrder...)
|
|
sort.SliceStable(cities, func(i, j int) bool {
|
|
return serversHaveFavorite(g.cities[cities[i]], favServers) &&
|
|
!serversHaveFavorite(g.cities[cities[j]], favServers)
|
|
})
|
|
return cities
|
|
}
|
|
|
|
func orderServersWithFavoritesFirst(servers []backend.LogicalServer, favServers map[string]bool) []backend.LogicalServer {
|
|
ordered := append([]backend.LogicalServer(nil), servers...)
|
|
sort.SliceStable(ordered, func(i, j int) bool {
|
|
return serverIsFavorite(ordered[i], favServers) && !serverIsFavorite(ordered[j], favServers)
|
|
})
|
|
return ordered
|
|
}
|
|
|
|
func flattenRows(tree []*countryGroup, expandedCountries, expandedCities, favCountries, favServers map[string]bool) []row {
|
|
ordered := orderWithFavoritesFirst(tree, favCountries, favServers)
|
|
|
|
var rows []row
|
|
for _, g := range ordered {
|
|
rows = append(rows, row{
|
|
kind: rowCountry,
|
|
country: g.code,
|
|
expanded: expandedCountries[g.code],
|
|
favorite: countryIsFavorite(g, favCountries, favServers),
|
|
serverCount: len(g.servers),
|
|
avgLoad: avgLoad(g.servers),
|
|
})
|
|
if !expandedCountries[g.code] {
|
|
continue
|
|
}
|
|
for _, city := range orderCitiesWithFavoritesFirst(g, favServers) {
|
|
servers := orderServersWithFavoritesFirst(g.cities[city], favServers)
|
|
key := cityKey(g.code, city)
|
|
rows = append(rows, row{
|
|
kind: rowCity,
|
|
country: g.code,
|
|
city: city,
|
|
expanded: expandedCities[key],
|
|
favorite: serversHaveFavorite(servers, favServers),
|
|
serverCount: len(servers),
|
|
avgLoad: avgLoad(servers),
|
|
})
|
|
if !expandedCities[key] {
|
|
continue
|
|
}
|
|
for _, s := range servers {
|
|
rows = append(rows, row{
|
|
kind: rowServer,
|
|
country: g.code,
|
|
city: city,
|
|
server: s,
|
|
favorite: serverIsFavorite(s, favServers),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return rows
|
|
}
|
|
|
|
type cityMatch struct {
|
|
city string
|
|
servers []backend.LogicalServer
|
|
}
|
|
|
|
func searchRows(tree []*countryGroup, query string, expandedCountries, expandedCities, favCountries, favServers map[string]bool) []row {
|
|
q := strings.ToLower(strings.TrimSpace(query))
|
|
if q == "" {
|
|
return nil
|
|
}
|
|
ordered := orderWithFavoritesFirst(tree, favCountries, favServers)
|
|
matchCities := len([]rune(q)) > 3
|
|
|
|
var rows []row
|
|
for _, g := range ordered {
|
|
countryMatch := strings.Contains(strings.ToLower(backend.CountryName(g.code)), q) ||
|
|
strings.Contains(strings.ToLower(g.code), q)
|
|
|
|
var matches []cityMatch
|
|
if matchCities {
|
|
for _, city := range g.cityOrder {
|
|
if servers := matchingCityServers(city, g.cities[city], q); len(servers) > 0 {
|
|
matches = append(matches, cityMatch{city: city, servers: servers})
|
|
}
|
|
}
|
|
}
|
|
|
|
if !countryMatch && len(matches) == 0 {
|
|
continue
|
|
}
|
|
|
|
expanded := len(matches) > 0
|
|
if v, ok := expandedCountries[g.code]; ok {
|
|
expanded = v
|
|
}
|
|
|
|
rows = append(rows, row{
|
|
kind: rowCountry,
|
|
country: g.code,
|
|
expanded: expanded,
|
|
favorite: countryIsFavorite(g, favCountries, favServers),
|
|
serverCount: len(g.servers),
|
|
avgLoad: avgLoad(g.servers),
|
|
})
|
|
if !expanded {
|
|
continue
|
|
}
|
|
|
|
if len(matches) == 0 {
|
|
for _, city := range g.cityOrder {
|
|
matches = append(matches, cityMatch{city: city, servers: g.cities[city]})
|
|
}
|
|
}
|
|
sort.SliceStable(matches, func(i, j int) bool {
|
|
return serversHaveFavorite(matches[i].servers, favServers) &&
|
|
!serversHaveFavorite(matches[j].servers, favServers)
|
|
})
|
|
for _, cm := range matches {
|
|
cityExpanded := true
|
|
if v, ok := expandedCities[cityKey(g.code, cm.city)]; ok {
|
|
cityExpanded = v
|
|
}
|
|
servers := orderServersWithFavoritesFirst(cm.servers, favServers)
|
|
rows = append(rows, row{
|
|
kind: rowCity,
|
|
country: g.code,
|
|
city: cm.city,
|
|
expanded: cityExpanded,
|
|
favorite: serversHaveFavorite(servers, favServers),
|
|
serverCount: len(servers),
|
|
avgLoad: avgLoad(servers),
|
|
})
|
|
if !cityExpanded {
|
|
continue
|
|
}
|
|
for _, s := range servers {
|
|
rows = append(rows, row{
|
|
kind: rowServer,
|
|
country: g.code,
|
|
city: cm.city,
|
|
server: s,
|
|
favorite: serverIsFavorite(s, favServers),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return rows
|
|
}
|
|
|
|
func matchingCityServers(city string, servers []backend.LogicalServer, q string) []backend.LogicalServer {
|
|
label := city
|
|
if label == "" {
|
|
label = "other"
|
|
}
|
|
if strings.Contains(strings.ToLower(label), q) {
|
|
return servers
|
|
}
|
|
var matched []backend.LogicalServer
|
|
for _, s := range servers {
|
|
if strings.Contains(strings.ToLower(s.Name), q) {
|
|
matched = append(matched, s)
|
|
}
|
|
}
|
|
return matched
|
|
}
|