ImportSubscription no longer requires only http(s): paste awg://, AWG .conf/JSON, or naive links; subscription parser also reads hosted AWG and Clash wireguard/naive. Site lists support emails and FAQ; Store listing texts/tools updated. Co-authored-by: Cursor <cursoragent@cursor.com>
186 lines
4.8 KiB
Go
186 lines
4.8 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/csv"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
func setField(all [][]string, header []string, field, col, value string) {
|
|
colIdx := -1
|
|
for i, h := range header {
|
|
if h == col {
|
|
colIdx = i
|
|
break
|
|
}
|
|
}
|
|
if colIdx < 0 {
|
|
panic(col)
|
|
}
|
|
for i := 1; i < len(all); i++ {
|
|
for len(all[i]) < len(header) {
|
|
all[i] = append(all[i], "")
|
|
}
|
|
if all[i][0] == field {
|
|
all[i][colIdx] = value
|
|
return
|
|
}
|
|
}
|
|
panic("missing " + field)
|
|
}
|
|
|
|
func getField(all [][]string, header []string, field, col string) string {
|
|
colIdx := -1
|
|
for i, h := range header {
|
|
if h == col {
|
|
colIdx = i
|
|
break
|
|
}
|
|
}
|
|
for i := 1; i < len(all); i++ {
|
|
if len(all[i]) > 0 && all[i][0] == field && colIdx >= 0 && colIdx < len(all[i]) {
|
|
return all[i][colIdx]
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func main() {
|
|
root := `D:\vpn navi`
|
|
path := filepath.Join(root, "store.csv")
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
r := csv.NewReader(f)
|
|
r.FieldsPerRecord = -1
|
|
r.LazyQuotes = true
|
|
all, err := r.ReadAll()
|
|
f.Close()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
header := all[0]
|
|
|
|
// Relative paths for Partner Center: upload folder with store.csv + screen/
|
|
ruShots := []string{
|
|
"screen/1.ru.png",
|
|
"screen/2.ru.png",
|
|
"screen/3.ru.png",
|
|
"screen/4.ru.png",
|
|
"screen/5.ru.png",
|
|
"screen/6.ru.png",
|
|
"screen/12dark.png",
|
|
"screen/13dark.png",
|
|
"screen/14dark.png",
|
|
"screen/15dark.png",
|
|
"screen/16dark.png",
|
|
"screen/9.png",
|
|
}
|
|
enShots := []string{
|
|
"screen/7.en.png",
|
|
"screen/8.en.png",
|
|
"screen/9.png",
|
|
"screen/10.en.png",
|
|
"screen/11.en.png",
|
|
"screen/12dark.png",
|
|
"screen/13dark.png",
|
|
"screen/14dark.png",
|
|
"screen/15dark.png",
|
|
"screen/16dark.png",
|
|
}
|
|
|
|
ruCaptions := [][2]string{
|
|
{"Главный экран: подключение и статус защиты", "Home: connect and protection status"},
|
|
{"Список серверов с флагами и пингом", "Server list with flags and ping"},
|
|
{"Настройки: тема, язык, Kill Switch", "Settings: theme, language, Kill Switch"},
|
|
{"Статистика использования", "Usage statistics"},
|
|
{"Помощь и полезные ссылки", "Help and useful links"},
|
|
{"Добавление конфигурации", "Add configuration"},
|
|
{"Тёмная тема: главный экран", "Dark theme: home"},
|
|
{"Тёмная тема: серверы", "Dark theme: servers"},
|
|
{"Тёмная тема: настройки", "Dark theme: settings"},
|
|
{"Тёмная тема: статистика", "Dark theme: statistics"},
|
|
{"Тёмная тема: помощь", "Dark theme: help"},
|
|
{"Общий вид приложения", "App overview"},
|
|
}
|
|
enCaptions := []string{
|
|
"Home: connect and protection status",
|
|
"Server list with flags and ping",
|
|
"App overview",
|
|
"Settings: theme, language, Kill Switch",
|
|
"Statistics and help",
|
|
"Dark theme: home",
|
|
"Dark theme: servers",
|
|
"Dark theme: settings",
|
|
"Dark theme: statistics",
|
|
"Dark theme: help",
|
|
}
|
|
|
|
for i, p := range ruShots {
|
|
setField(all, header, fmt.Sprintf("DesktopScreenshot%d", i+1), "default", p)
|
|
}
|
|
for i, p := range enShots {
|
|
setField(all, header, fmt.Sprintf("DesktopScreenshot%d", i+1), "en", p)
|
|
}
|
|
// Clear en slots beyond enShots so Partner Center falls back to default for RU-only extras
|
|
for i := len(enShots); i < 30; i++ {
|
|
setField(all, header, fmt.Sprintf("DesktopScreenshot%d", i+1), "en", "")
|
|
}
|
|
// Clear default slots beyond ruShots
|
|
for i := len(ruShots); i < 30; i++ {
|
|
setField(all, header, fmt.Sprintf("DesktopScreenshot%d", i+1), "default", "")
|
|
}
|
|
|
|
for i, c := range ruCaptions {
|
|
field := fmt.Sprintf("DesktopScreenshotCaption%d", i+1)
|
|
setField(all, header, field, "default", c[0])
|
|
en := c[1]
|
|
if i < len(enCaptions) {
|
|
en = enCaptions[i]
|
|
}
|
|
setField(all, header, field, "en", en)
|
|
}
|
|
for i := len(ruCaptions); i < 30; i++ {
|
|
field := fmt.Sprintf("DesktopScreenshotCaption%d", i+1)
|
|
setField(all, header, field, "default", "")
|
|
setField(all, header, field, "en", "")
|
|
}
|
|
|
|
out, err := os.Create(path)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
if _, err := out.Write([]byte{0xEF, 0xBB, 0xBF}); err != nil {
|
|
panic(err)
|
|
}
|
|
w := csv.NewWriter(out)
|
|
if err := w.WriteAll(all); err != nil {
|
|
panic(err)
|
|
}
|
|
w.Flush()
|
|
out.Close()
|
|
|
|
for i := 1; i <= 12; i++ {
|
|
d := getField(all, header, fmt.Sprintf("DesktopScreenshot%d", i), "default")
|
|
e := getField(all, header, fmt.Sprintf("DesktopScreenshot%d", i), "en")
|
|
if d == "" && e == "" {
|
|
continue
|
|
}
|
|
fmt.Printf("%d default=%s en=%s\n", i, d, e)
|
|
for _, p := range []string{d, e} {
|
|
if p == "" {
|
|
continue
|
|
}
|
|
full := filepath.Join(root, filepath.FromSlash(p))
|
|
if _, err := os.Stat(full); err != nil {
|
|
fmt.Println(" MISSING", full)
|
|
}
|
|
}
|
|
}
|
|
fmt.Println("updated", path, "rows", len(all))
|
|
_ = strings.TrimSpace
|
|
}
|