mirror of
https://github.com/anotherhadi/proton-vpn-tui.git
synced 2026-10-05 19:28:28 +02:00
init
This commit is contained in:
@@ -0,0 +1,318 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user