├── .github
├── resources
│ ├── banner.png
│ └── screenshots
│ │ ├── browse.jpg
│ │ ├── main_menu.jpg
│ │ ├── mortar_1.jpg
│ │ ├── mortar_2.jpg
│ │ ├── mortar_3.jpg
│ │ ├── portmaster_1.jpg
│ │ ├── portmaster_2.jpg
│ │ ├── ports.jpg
│ │ └── updates.jpg
└── workflows
│ ├── build_pak.yml
│ └── build_storefront.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── app
├── pak_store.go
└── storefront_builder.go
├── database
├── db.go
├── functions.go
├── models.go
└── queries.sql.go
├── dev_scripts
├── attach-debugger.sh
├── kill_pak_store.sh
└── push-binary.sh
├── embed.go
├── go.mod
├── go.sum
├── go.work
├── go.work.sum
├── launch.sh
├── models
├── constants.go
├── database.go
├── menu.go
├── pak.go
├── screens.go
└── storefront.go
├── pak.json
├── resources
└── splash.png
├── sql
├── queries.sql
└── schema.sql
├── sqlc.yaml
├── state
└── app_state.go
├── storefront_base.json
├── taskfile.yml
├── ui
├── browse.go
├── main_menu.go
├── manage.go
├── pak_info.go
├── pak_list.go
└── updates.go
└── utils
└── functions.go
/.github/resources/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UncleJunVIP/nextui-pak-store/0350f3036c346cedceb670b10b86c6914a3c758a/.github/resources/banner.png
--------------------------------------------------------------------------------
/.github/resources/screenshots/browse.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UncleJunVIP/nextui-pak-store/0350f3036c346cedceb670b10b86c6914a3c758a/.github/resources/screenshots/browse.jpg
--------------------------------------------------------------------------------
/.github/resources/screenshots/main_menu.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UncleJunVIP/nextui-pak-store/0350f3036c346cedceb670b10b86c6914a3c758a/.github/resources/screenshots/main_menu.jpg
--------------------------------------------------------------------------------
/.github/resources/screenshots/mortar_1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UncleJunVIP/nextui-pak-store/0350f3036c346cedceb670b10b86c6914a3c758a/.github/resources/screenshots/mortar_1.jpg
--------------------------------------------------------------------------------
/.github/resources/screenshots/mortar_2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UncleJunVIP/nextui-pak-store/0350f3036c346cedceb670b10b86c6914a3c758a/.github/resources/screenshots/mortar_2.jpg
--------------------------------------------------------------------------------
/.github/resources/screenshots/mortar_3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UncleJunVIP/nextui-pak-store/0350f3036c346cedceb670b10b86c6914a3c758a/.github/resources/screenshots/mortar_3.jpg
--------------------------------------------------------------------------------
/.github/resources/screenshots/portmaster_1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UncleJunVIP/nextui-pak-store/0350f3036c346cedceb670b10b86c6914a3c758a/.github/resources/screenshots/portmaster_1.jpg
--------------------------------------------------------------------------------
/.github/resources/screenshots/portmaster_2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UncleJunVIP/nextui-pak-store/0350f3036c346cedceb670b10b86c6914a3c758a/.github/resources/screenshots/portmaster_2.jpg
--------------------------------------------------------------------------------
/.github/resources/screenshots/ports.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UncleJunVIP/nextui-pak-store/0350f3036c346cedceb670b10b86c6914a3c758a/.github/resources/screenshots/ports.jpg
--------------------------------------------------------------------------------
/.github/resources/screenshots/updates.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UncleJunVIP/nextui-pak-store/0350f3036c346cedceb670b10b86c6914a3c758a/.github/resources/screenshots/updates.jpg
--------------------------------------------------------------------------------
/.github/workflows/build_pak.yml:
--------------------------------------------------------------------------------
1 | name: Package Pak Store
2 |
3 | on:
4 | release:
5 | types: [ published ]
6 | workflow_dispatch:
7 |
8 | jobs:
9 |
10 | build:
11 | runs-on: ubuntu-22.04-arm
12 | steps:
13 | - uses: actions/checkout@v4
14 |
15 | - name: Install Task
16 | uses: arduino/setup-task@v2
17 | with:
18 | version: 3.x
19 | repo-token: ${{ secrets.GITHUB_TOKEN }}
20 |
21 | - name: Build and Package
22 | run: task build package
23 |
24 | - uses: actions/upload-artifact@v4
25 | with:
26 | name: "Pak Store.pak"
27 | path: "build/Pak Store.pak"
28 | if-no-files-found: error
29 | retention-days: 3
30 | overwrite: true
31 |
--------------------------------------------------------------------------------
/.github/workflows/build_storefront.yml:
--------------------------------------------------------------------------------
1 | name: Build Storefront.json
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | paths:
7 | - storefront_base.json
8 | - pak.json
9 | - app/storefront_builder.go
10 | schedule:
11 | - cron: "0 * * * *"
12 |
13 | jobs:
14 |
15 | build:
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@v4
19 |
20 | - name: Set up Go
21 | uses: actions/setup-go@v4
22 | with:
23 | go-version: '1.24.1'
24 |
25 | - name: Build Storefront.json
26 | run: go run app/storefront_builder.go
27 | env:
28 | GOWORK: off
29 | GH_TOKEN: ${{ secrets.GH_TOKEN }}
30 |
31 | - name: Configure AWS credentials
32 | uses: aws-actions/configure-aws-credentials@v2
33 | with:
34 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
35 | aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
36 | aws-region: "us-east-1"
37 |
38 | - name: Upload to S3
39 | run: aws s3 cp storefront.json s3://pak-store.unclejun.vip/storefront.json
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea/
2 | /*.iml
3 | *.log
4 | /.device_logs/
5 | /pak-store.db
6 | /pak-store
7 | /storefront.json
8 | /bin/tg5040/minui-keyboard
9 | /bin/tg5040/minui-list
10 | /bin/tg5040/minui-presenter
11 | /certs/certificates.crt
12 | /data/systems-mapping.json
13 | .DS_Store
14 | **/.DS_Store
15 | /resources/fonts/
16 | /build/*
17 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.24-bullseye
2 |
3 | RUN apt-get update && apt-get install -y \
4 | libsdl2-dev \
5 | libsdl2-ttf-dev \
6 | libsdl2-image-dev \
7 | libsdl2-gfx-dev
8 |
9 | WORKDIR /build
10 |
11 | COPY go.mod go.sum* ./
12 |
13 | RUN GOWORK=off go mod download
14 |
15 | COPY . .
16 | RUN GOWORK=off go build -v -gcflags="all=-N -l" -o pak-store app/pak_store.go
17 |
18 | CMD ["/bin/bash"]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Brandon T. Kowalski
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 | 
5 | 
6 | 
7 | 
8 |
9 |
10 |
11 | ---
12 |
13 | ## How do I setup Pak Store?
14 |
15 | 1. Own a TrimUI Brick or Smart Pro and have a SD Card with NextUI configured.
16 | 2. Connect your device to a Wi-Fi network.
17 | 3. Download the latest Pak Store release from this repo.
18 | 4. Unzip the release download.
19 | - If the unzipped folder name is `Pak.Store.pak` please rename it to `Pak Store.pak`.
20 | 5. Copy the entire `Pak Store.pak` folder to `SD_ROOT/Tools/tg5040`.
21 | 6. Reinsert your SD Card into your device.
22 | 7. Launch `Pak Store` from the `Tools` menu and enjoy all the amazing Paks made by the community!
23 |
24 | ---
25 |
26 | ## I want my Pak in Pak Store!
27 |
28 | Awesome! To get added to Pak Store you have to complete the following steps:
29 |
30 | 1. Create a `pak.json` file at the root of your repo. An example can be seen below.
31 | 2. Make sure your release is tagged properly and matches the version number in `pak.json`.
32 | 3. Make sure the file name of the release artifact matches what is in `pak.json`.
33 | 4. Once all of these steps are complete, please file an issue with a link to your repo.
34 |
35 | ---
36 |
37 | ## Sample pak.json
38 | ```json
39 | {
40 | "name": "Pak Store",
41 | "version": "v1.0.1",
42 | "type": "TOOL | EMU",
43 | "description": "A Pak Store in this economy?!",
44 | "author": "K-Wall",
45 | "repo_url": "https://github.com/UncleJunVIP/nextui-pak-store",
46 | "release_filename": "Pak.Store.pak.zip",
47 | "changelog": {
48 | "v1.0.0": "Upgraded the UI to use gabagool, my NextUI Pak UI Library!"
49 | },
50 | "update_ignore": [
51 | "path/of/file/to/ignore"
52 | ],
53 | "screenshots": [
54 | ".github/resources/screenshots/main_menu.jpg",
55 | ".github/resources/screenshots/browse.jpg",
56 | ".github/resources/screenshots/ports.jpg",
57 | ".github/resources/screenshots/portmaster_1.jpg",
58 | ".github/resources/screenshots/portmaster_2.jpg",
59 | ".github/resources/screenshots/updates.jpg",
60 | ".github/resources/screenshots/mortar_1.jpg",
61 | ".github/resources/screenshots/mortar_2.jpg",
62 | ".github/resources/screenshots/mortar_3.jpg"
63 | ],
64 | "platforms": [
65 | "tg5040"
66 | ]
67 | }
68 | ```
69 |
70 | ---
71 |
72 | Enjoy! ✌🏻
--------------------------------------------------------------------------------
/app/pak_store.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | _ "embed"
5 | _ "github.com/UncleJunVIP/certifiable"
6 | gaba "github.com/UncleJunVIP/gabagool/pkg/gabagool"
7 | "github.com/UncleJunVIP/nextui-pak-shared-functions/common"
8 | "github.com/UncleJunVIP/nextui-pak-store/database"
9 | "github.com/UncleJunVIP/nextui-pak-store/models"
10 | "github.com/UncleJunVIP/nextui-pak-store/state"
11 | "github.com/UncleJunVIP/nextui-pak-store/ui"
12 | "github.com/UncleJunVIP/nextui-pak-store/utils"
13 | _ "modernc.org/sqlite"
14 | "os"
15 | "time"
16 | )
17 |
18 | var appState state.AppState
19 |
20 | func init() {
21 | gaba.InitSDL(gaba.GabagoolOptions{
22 | WindowTitle: "Pak Store",
23 | ShowBackground: true,
24 | })
25 | common.SetLogLevel("ERROR")
26 |
27 | if !utils.IsConnectedToInternet() {
28 | gaba.ConfirmationMessage("No Internet Connection!\nMake sure you are connected to Wi-Fi.", []gaba.FooterHelpItem{
29 | {ButtonName: "B", HelpText: "Quit"},
30 | }, gaba.MessageOptions{})
31 | defer cleanup()
32 | common.LogStandardFatal("No Internet Connection", nil)
33 | }
34 |
35 | sf, err := gaba.ProcessMessage("",
36 | gaba.ProcessMessageOptions{Image: "resources/splash.png", ImageWidth: 1024, ImageHeight: 768}, func() (interface{}, error) {
37 | time.Sleep(1250 * time.Millisecond)
38 | return utils.FetchStorefront(models.StorefrontJson)
39 | })
40 |
41 | if err != nil {
42 | gaba.ConfirmationMessage("Could not load the Storefront!\nPlease check the logs for more info.", []gaba.FooterHelpItem{
43 | {ButtonName: "B", HelpText: "Quit"},
44 | }, gaba.MessageOptions{})
45 | defer gaba.CloseSDL()
46 | common.LogStandardFatal("Could not load Storefront!", err)
47 | }
48 |
49 | appState = state.NewAppState(sf.Result.(models.Storefront))
50 | }
51 |
52 | func cleanup() {
53 | database.CloseDB()
54 | common.CloseLogger()
55 | }
56 |
57 | func main() {
58 | defer gaba.CloseSDL()
59 | defer cleanup()
60 |
61 | logger := common.GetLoggerInstance()
62 |
63 | logger.Info("Starting Pak Store")
64 |
65 | var screen models.Screen
66 | screen = ui.InitMainMenu(appState)
67 |
68 | for {
69 | res, code, _ := screen.Draw()
70 |
71 | if code == 23 {
72 | gaba.ProcessMessage("Pak Store Updated! Exiting...", gaba.ProcessMessageOptions{}, func() (interface{}, error) {
73 | time.Sleep(3 * time.Second)
74 | return nil, nil
75 | })
76 | }
77 |
78 | switch screen.Name() {
79 | case models.ScreenNames.MainMenu:
80 | switch code {
81 | case 0:
82 | switch res.(string) {
83 | case "Browse":
84 | screen = ui.InitBrowseScreen(appState)
85 | case "Available Updates":
86 | screen = ui.InitUpdatesScreen(appState)
87 | case "Manage Installed":
88 | screen = ui.InitManageInstalledScreen(appState)
89 | }
90 | case 4:
91 | appState = appState.Refresh()
92 | screen = ui.InitMainMenu(appState)
93 | case 1, 2:
94 | os.Exit(0)
95 | }
96 |
97 | case models.ScreenNames.Browse:
98 | switch code {
99 | case 0:
100 | state.LastSelectedIndex = 0
101 | state.LastSelectedPosition = 0
102 | screen = ui.InitPakList(appState, res.(string))
103 | case 1, 2:
104 | screen = ui.InitMainMenu(appState)
105 | }
106 |
107 | case models.ScreenNames.PakList:
108 | switch code {
109 | case 0:
110 | screen = ui.InitPakInfoScreen(res.(models.Pak), screen.(ui.PakList).Category, false)
111 | case 1, 2:
112 | screen = ui.InitBrowseScreen(appState)
113 | }
114 |
115 | case models.ScreenNames.PakInfo:
116 | switch code {
117 | case 0, 1, 2, 4:
118 | appState = appState.Refresh()
119 |
120 | if res.(bool) {
121 | if len(appState.UpdatesAvailable) == 0 {
122 | screen = ui.InitMainMenu(appState)
123 | break
124 | }
125 |
126 | screen = ui.InitUpdatesScreen(appState)
127 | } else {
128 | if len(appState.AvailablePaks) == 0 {
129 | screen = ui.InitBrowseScreen(appState)
130 | break
131 | }
132 |
133 | if len(appState.BrowsePaks[screen.(ui.PakInfoScreen).Category]) == 0 {
134 | screen = ui.InitBrowseScreen(appState)
135 | break
136 | }
137 | screen = ui.InitPakList(appState, screen.(ui.PakInfoScreen).Category)
138 | }
139 | case -1:
140 | gaba.ProcessMessage("Unable to Download Pak!", gaba.ProcessMessageOptions{ShowThemeBackground: true}, func() (interface{}, error) {
141 | time.Sleep(1750 * time.Millisecond)
142 | return nil, nil
143 | })
144 | break
145 | case 86:
146 | break
147 | }
148 |
149 | case models.ScreenNames.Updates:
150 | switch code {
151 | case 0:
152 | appState = appState.Refresh()
153 | screen = ui.InitPakInfoScreen(res.(models.Pak), "", true)
154 | case 1, 2:
155 | appState = appState.Refresh()
156 | screen = ui.InitMainMenu(appState)
157 | }
158 |
159 | case models.ScreenNames.ManageInstalled:
160 | switch code {
161 | case 0, 11, 12:
162 | appState = appState.Refresh()
163 |
164 | if len(appState.InstalledPaks) == 0 {
165 | screen = ui.InitMainMenu(appState)
166 | break
167 | }
168 |
169 | screen = ui.InitManageInstalledScreen(appState)
170 | case 1, 2:
171 | appState = appState.Refresh()
172 | screen = ui.InitMainMenu(appState)
173 | }
174 |
175 | }
176 | }
177 |
178 | }
179 |
--------------------------------------------------------------------------------
/app/storefront_builder.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/base64"
5 | "encoding/json"
6 | "fmt"
7 | "github.com/UncleJunVIP/nextui-pak-store/models"
8 | "io"
9 | "log"
10 | "net/http"
11 | "os"
12 | "strings"
13 | )
14 |
15 | type GitHubContent struct {
16 | Name string `json:"name"`
17 | Path string `json:"path"`
18 | Encoding string `json:"encoding"`
19 | Content string `json:"content"`
20 | DownloadUrl string `json:"download_url"`
21 | }
22 |
23 | func main() {
24 | data, err := os.ReadFile("storefront_base.json")
25 | if err != nil {
26 | log.Fatal("Error reading file:", err)
27 | }
28 |
29 | var sf models.Storefront
30 | if err := json.Unmarshal(data, &sf); err != nil {
31 | log.Fatal("Unable to unmarshal storefront", err)
32 | }
33 |
34 | var paks []models.Pak
35 |
36 | for _, p := range sf.Paks {
37 | if p.Disabled {
38 | continue
39 | }
40 |
41 | repoPath := strings.ReplaceAll(p.RepoURL, models.GitHubRoot, "")
42 | parts := strings.Split(repoPath, "/")
43 | if len(parts) < 2 {
44 | log.Fatal("Invalid repository URL format:", p.RepoURL)
45 | }
46 |
47 | owner := parts[0]
48 | repo := parts[1]
49 |
50 | apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/contents/%s",
51 | owner, repo, models.PakJsonStub)
52 |
53 | pak, err := fetchPakJsonFromGitHubAPI(apiURL)
54 | if err != nil {
55 | log.Fatal("Unable to fetch pak json for "+p.Name+" ("+p.RepoURL+")", err)
56 | }
57 |
58 | pak.StorefrontName = p.StorefrontName
59 | pak.RepoURL = p.RepoURL
60 | pak.Categories = p.Categories
61 | pak.LargePak = p.LargePak
62 |
63 | paks = append(paks, pak)
64 | }
65 |
66 | sf.Paks = paks
67 |
68 | jsonData, err := json.MarshalIndent(sf, "", " ")
69 | if err != nil {
70 | log.Fatal("Unable to marshal storefront to JSON", err)
71 | }
72 |
73 | err = os.WriteFile("storefront.json", jsonData, 0644)
74 | if err != nil {
75 | log.Fatal("Unable to write storefront.json", err)
76 | }
77 | }
78 |
79 | func fetchPakJsonFromGitHubAPI(apiURL string) (models.Pak, error) {
80 | var pak models.Pak
81 |
82 | req, err := http.NewRequest("GET", apiURL, nil)
83 | if err != nil {
84 | return pak, fmt.Errorf("error creating HTTP request: %w", err)
85 | }
86 |
87 | req.Header.Add("Accept", "application/vnd.github.v3+json")
88 |
89 | req.Header.Add("Authorization", "Bearer "+os.Getenv("GH_TOKEN"))
90 |
91 | client := &http.Client{}
92 | resp, err := client.Do(req)
93 | if err != nil {
94 | return pak, fmt.Errorf("error making HTTP request: %w", err)
95 | }
96 | defer resp.Body.Close()
97 |
98 | if resp.StatusCode != http.StatusOK {
99 | body, _ := io.ReadAll(resp.Body)
100 | return pak, fmt.Errorf("GitHub API error: %s - %s", resp.Status, string(body))
101 | }
102 |
103 | var content GitHubContent
104 | if err := json.NewDecoder(resp.Body).Decode(&content); err != nil {
105 | return pak, fmt.Errorf("error decoding GitHub API response: %w", err)
106 | }
107 |
108 | if content.Encoding == "base64" {
109 | contentBytes, err := base64.StdEncoding.DecodeString(
110 | strings.ReplaceAll(content.Content, "\n", ""))
111 | if err != nil {
112 | return pak, fmt.Errorf("error decoding base64 content: %w", err)
113 | }
114 |
115 | if err := json.Unmarshal(contentBytes, &pak); err != nil {
116 | return pak, fmt.Errorf("error parsing pak.json: %w", err)
117 | }
118 | } else {
119 | return pak, fmt.Errorf("unexpected content encoding: %s", content.Encoding)
120 | }
121 |
122 | return pak, nil
123 | }
124 |
--------------------------------------------------------------------------------
/database/db.go:
--------------------------------------------------------------------------------
1 | // Code generated by sqlc. DO NOT EDIT.
2 | // versions:
3 | // sqlc v1.29.0
4 |
5 | package database
6 |
7 | import (
8 | "context"
9 | "database/sql"
10 | )
11 |
12 | type DBTX interface {
13 | ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
14 | PrepareContext(context.Context, string) (*sql.Stmt, error)
15 | QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
16 | QueryRowContext(context.Context, string, ...interface{}) *sql.Row
17 | }
18 |
19 | func New(db DBTX) *Queries {
20 | return &Queries{db: db}
21 | }
22 |
23 | type Queries struct {
24 | db DBTX
25 | }
26 |
27 | func (q *Queries) WithTx(tx *sql.Tx) *Queries {
28 | return &Queries{
29 | db: tx,
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/database/functions.go:
--------------------------------------------------------------------------------
1 | package database
2 |
3 | import (
4 | "context"
5 | "database/sql"
6 | _ "embed"
7 | "errors"
8 | "github.com/UncleJunVIP/nextui-pak-shared-functions/common"
9 | pakstore "github.com/UncleJunVIP/nextui-pak-store"
10 | "github.com/UncleJunVIP/nextui-pak-store/models"
11 | "github.com/UncleJunVIP/nextui-pak-store/utils"
12 | "go.uber.org/zap"
13 | "log"
14 | _ "modernc.org/sqlite"
15 | "os"
16 | "path/filepath"
17 | )
18 |
19 | var dbc *sql.DB
20 | var queries *Queries
21 |
22 | func init() {
23 | logger := common.GetLoggerInstance()
24 | ctx := context.Background()
25 |
26 | var err error
27 | dbPath := filepath.Join(models.PakStoreConfigRoot, "pak-store.db")
28 |
29 | if os.Getenv("ENVIRONMENT") == "DEV" {
30 | dbPath = "pak-store.db"
31 | }
32 |
33 | dbDir := filepath.Dir(dbPath)
34 | if dbDir != "." && dbDir != "" {
35 | err := os.MkdirAll(dbDir, 0755)
36 | if err != nil {
37 | //_, _ = cui.ShowMessage(models.InitializationError, "3")
38 | logger.Fatal("Unable to open database file", zap.Error(err))
39 | }
40 | }
41 |
42 | dbc, err = sql.Open("sqlite", "file:"+dbPath)
43 | if err != nil {
44 | //_, _ = cui.ShowMessage(models.InitializationError, "3")
45 | logger.Fatal("Unable to open database file", zap.Error(err))
46 | }
47 |
48 | schemaExists, err := TableExists(dbc, "installed_paks")
49 | if !schemaExists {
50 | if _, err := dbc.ExecContext(ctx, pakstore.DDL); err != nil {
51 | //_, _ = cui.ShowMessage(models.InitializationError, "3")
52 | logger.Fatal("Unable to init schema", zap.Error(err))
53 | }
54 | }
55 |
56 | queries = New(dbc)
57 |
58 | if !schemaExists {
59 | var pak models.Pak
60 | err := utils.ParseJSONFile("pak.json", &pak)
61 | if err != nil {
62 | log.Fatalf("Error parsing JSON file: %v", err)
63 | }
64 |
65 | queries.Install(ctx, InstallParams{
66 | DisplayName: "Pak Store",
67 | Name: "Pak Store",
68 | Version: pak.Version,
69 | Type: "TOOL",
70 | })
71 | }
72 | }
73 |
74 | func DBQ() *Queries {
75 | return queries
76 | }
77 | func CloseDB() {
78 | _ = dbc.Close()
79 | }
80 |
81 | func TableExists(db *sql.DB, tableName string) (bool, error) {
82 | query := `SELECT name FROM sqlite_master WHERE type='table' AND name=?`
83 | var name string
84 | err := db.QueryRow(query, tableName).Scan(&name)
85 | if errors.Is(err, sql.ErrNoRows) {
86 | return false, nil
87 | }
88 | return err == nil, err
89 | }
90 |
--------------------------------------------------------------------------------
/database/models.go:
--------------------------------------------------------------------------------
1 | // Code generated by sqlc. DO NOT EDIT.
2 | // versions:
3 | // sqlc v1.29.0
4 |
5 | package database
6 |
7 | type InstalledPak struct {
8 | Name string
9 | DisplayName string
10 | Type string
11 | Version string
12 | CanUninstall int64
13 | }
14 |
--------------------------------------------------------------------------------
/database/queries.sql.go:
--------------------------------------------------------------------------------
1 | // Code generated by sqlc. DO NOT EDIT.
2 | // versions:
3 | // sqlc v1.29.0
4 | // source: queries.sql
5 |
6 | package database
7 |
8 | import (
9 | "context"
10 | )
11 |
12 | const install = `-- name: Install :exec
13 | INSERT INTO installed_paks (display_name, name, version, type, can_uninstall)
14 | VALUES (?, ?, ?, ?, ?)
15 | `
16 |
17 | type InstallParams struct {
18 | DisplayName string
19 | Name string
20 | Version string
21 | Type string
22 | CanUninstall int64
23 | }
24 |
25 | func (q *Queries) Install(ctx context.Context, arg InstallParams) error {
26 | _, err := q.db.ExecContext(ctx, install,
27 | arg.DisplayName,
28 | arg.Name,
29 | arg.Version,
30 | arg.Type,
31 | arg.CanUninstall,
32 | )
33 | return err
34 | }
35 |
36 | const listInstalledPaks = `-- name: ListInstalledPaks :many
37 | SELECT name, display_name, type, version, can_uninstall
38 | FROM installed_paks
39 | ORDER BY name
40 | `
41 |
42 | func (q *Queries) ListInstalledPaks(ctx context.Context) ([]InstalledPak, error) {
43 | rows, err := q.db.QueryContext(ctx, listInstalledPaks)
44 | if err != nil {
45 | return nil, err
46 | }
47 | defer rows.Close()
48 | var items []InstalledPak
49 | for rows.Next() {
50 | var i InstalledPak
51 | if err := rows.Scan(
52 | &i.Name,
53 | &i.DisplayName,
54 | &i.Type,
55 | &i.Version,
56 | &i.CanUninstall,
57 | ); err != nil {
58 | return nil, err
59 | }
60 | items = append(items, i)
61 | }
62 | if err := rows.Close(); err != nil {
63 | return nil, err
64 | }
65 | if err := rows.Err(); err != nil {
66 | return nil, err
67 | }
68 | return items, nil
69 | }
70 |
71 | const uninstall = `-- name: Uninstall :exec
72 | DELETE
73 | FROM installed_paks
74 | WHERE name = ?
75 | `
76 |
77 | func (q *Queries) Uninstall(ctx context.Context, name string) error {
78 | _, err := q.db.ExecContext(ctx, uninstall, name)
79 | return err
80 | }
81 |
82 | const updateVersion = `-- name: UpdateVersion :exec
83 | UPDATE installed_paks
84 | SET version = ?
85 | WHERE name = ?
86 | `
87 |
88 | type UpdateVersionParams struct {
89 | Version string
90 | Name string
91 | }
92 |
93 | func (q *Queries) UpdateVersion(ctx context.Context, arg UpdateVersionParams) error {
94 | _, err := q.db.ExecContext(ctx, updateVersion, arg.Version, arg.Name)
95 | return err
96 | }
97 |
--------------------------------------------------------------------------------
/dev_scripts/attach-debugger.sh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 | printf "Starting Pak Store Remote Debugger"
3 | while true; do
4 | sshpass -p 'tina' ssh root@192.168.1.210 "sh -c '/mnt/SDCARD/Developer/bin/dlv attach --headless --listen=:2345 --api-version=2 --accept-multiclient \$(pidof pak-store)'" > /dev/null 2>&1
5 | sleep 3
6 | done
7 |
--------------------------------------------------------------------------------
/dev_scripts/kill_pak_store.sh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 | printf "Shit broke! Killing Pak Store."
3 | sshpass -p 'tina' ssh root@192.168.1.16 "kill \$(pidof dlv)" > /dev/null 2>&1
4 | sshpass -p 'tina' ssh root@192.168.1.16 "kill \$(pidof pak-store)" > /dev/null 2>&1
5 |
--------------------------------------------------------------------------------
/dev_scripts/push-binary.sh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 | adb push ./pak-store "/mnt/SDCARD/Tools/tg5040/Pak Store.pak"
3 |
4 | adb shell rm "/mnt/SDCARD/Tools/tg5040/Pak\ Store.pak/pak-store.log" || true
5 |
6 | printf "Pak Store has been pushed to device!"
7 |
8 | printf "\a"
9 |
--------------------------------------------------------------------------------
/embed.go:
--------------------------------------------------------------------------------
1 | package nextui_pak_store
2 |
3 | import _ "embed"
4 |
5 | //go:embed sql/schema.sql
6 | var DDL string
7 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/UncleJunVIP/nextui-pak-store
2 |
3 | go 1.24
4 |
5 | require (
6 | github.com/UncleJunVIP/certifiable v1.0.0
7 | github.com/UncleJunVIP/gabagool v0.0.41
8 | github.com/UncleJunVIP/nextui-pak-shared-functions v1.7.1
9 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
10 | go.uber.org/zap v1.27.0
11 | golang.org/x/mod v0.24.0
12 | modernc.org/sqlite v1.37.1
13 | qlova.tech v0.1.1
14 | )
15 |
16 | require (
17 | github.com/PuerkitoBio/goquery v1.10.3 // indirect
18 | github.com/activcoding/HTML-Table-to-JSON v0.0.4 // indirect
19 | github.com/andybalholm/cascadia v1.3.3 // indirect
20 | github.com/dustin/go-humanize v1.0.1 // indirect
21 | github.com/google/uuid v1.6.0 // indirect
22 | github.com/kettek/apng v0.0.0-20220823221153-ff692776a607 // indirect
23 | github.com/mattn/go-isatty v0.0.20 // indirect
24 | github.com/ncruces/go-strftime v0.1.9 // indirect
25 | github.com/patrickhuber/go-types v0.6.0 // indirect
26 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
27 | github.com/veandco/go-sdl2 v0.4.40 // indirect
28 | go.uber.org/atomic v1.11.0 // indirect
29 | go.uber.org/multierr v1.11.0 // indirect
30 | golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
31 | golang.org/x/net v0.40.0 // indirect
32 | golang.org/x/sys v0.33.0 // indirect
33 | modernc.org/libc v1.65.8 // indirect
34 | modernc.org/mathutil v1.7.1 // indirect
35 | modernc.org/memory v1.11.0 // indirect
36 | )
37 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
2 | github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
3 | github.com/UncleJunVIP/certifiable v1.0.0 h1:g8SMZrGW74qLvli9j0CYO5BvpVcGhqxfvAUnTeSjyfc=
4 | github.com/UncleJunVIP/certifiable v1.0.0/go.mod h1:paPhCSE8DKzYvBcFcr5NVAYJmmvADq8e5fvkvs95np4=
5 | github.com/UncleJunVIP/gabagool v0.0.19 h1:Uek/qlbTVpEoNiWFJG4CnJiULyO2zv87PIUJxAsbHKk=
6 | github.com/UncleJunVIP/gabagool v0.0.19/go.mod h1:OQK3O4va4xT0hN3jfsG06Z7yaDGSqivAnRW3171j1MY=
7 | github.com/UncleJunVIP/gabagool v0.0.20 h1:a6D+0BREmbJ/5T5tvYY+f66xsbpq23ZZN/aWpQNHoCc=
8 | github.com/UncleJunVIP/gabagool v0.0.20/go.mod h1:AwCrrFLlQwwCQo0eVbPPF2dSEKqxOAyGGVS3tKFfIAk=
9 | github.com/UncleJunVIP/gabagool v0.0.21 h1:mLroJkiifO/HkdEt4hEH+IMBIGIsOMYxcVVoxc2sGHw=
10 | github.com/UncleJunVIP/gabagool v0.0.21/go.mod h1:AwCrrFLlQwwCQo0eVbPPF2dSEKqxOAyGGVS3tKFfIAk=
11 | github.com/UncleJunVIP/gabagool v0.0.22 h1:/56RynjSZfiZks6HIY/26XGMDmbm3j/FzFI804racyU=
12 | github.com/UncleJunVIP/gabagool v0.0.22/go.mod h1:AwCrrFLlQwwCQo0eVbPPF2dSEKqxOAyGGVS3tKFfIAk=
13 | github.com/UncleJunVIP/gabagool v0.0.23 h1:QbxZzn8JARmuiolG/ob1Z2vz494q0GT5+RI/ZyWphEs=
14 | github.com/UncleJunVIP/gabagool v0.0.23/go.mod h1:AwCrrFLlQwwCQo0eVbPPF2dSEKqxOAyGGVS3tKFfIAk=
15 | github.com/UncleJunVIP/gabagool v0.0.24 h1:dKvqHcV/oSWZ0cnb86bbZeHCOswcjt92j7bK3SOq8a4=
16 | github.com/UncleJunVIP/gabagool v0.0.24/go.mod h1:AwCrrFLlQwwCQo0eVbPPF2dSEKqxOAyGGVS3tKFfIAk=
17 | github.com/UncleJunVIP/gabagool v0.0.25 h1:tg06Obk+Ry47ZGvJk5R3czN2P0ddCmWT7REI0QIdxcM=
18 | github.com/UncleJunVIP/gabagool v0.0.25/go.mod h1:AwCrrFLlQwwCQo0eVbPPF2dSEKqxOAyGGVS3tKFfIAk=
19 | github.com/UncleJunVIP/gabagool v0.0.26 h1:XqwuhAUcZa+SfLHkrr0hwHBFsRhIuvI6Gc1XBCVFgHI=
20 | github.com/UncleJunVIP/gabagool v0.0.26/go.mod h1:AwCrrFLlQwwCQo0eVbPPF2dSEKqxOAyGGVS3tKFfIAk=
21 | github.com/UncleJunVIP/gabagool v0.0.27 h1:1vflQgiJrRPdVMFFxomboCFJuBGirq6pI8N7x6Xcsp0=
22 | github.com/UncleJunVIP/gabagool v0.0.27/go.mod h1:AwCrrFLlQwwCQo0eVbPPF2dSEKqxOAyGGVS3tKFfIAk=
23 | github.com/UncleJunVIP/gabagool v0.0.29 h1:yQDIClPx/8QQXbay4XTJyAVKO10hm+Zo3KInjQEv8CY=
24 | github.com/UncleJunVIP/gabagool v0.0.29/go.mod h1:AwCrrFLlQwwCQo0eVbPPF2dSEKqxOAyGGVS3tKFfIAk=
25 | github.com/UncleJunVIP/gabagool v0.0.30 h1:6pX3KTny1hjjCHKBNp4RtHKEnsXsdqegYpYc3HNFbuQ=
26 | github.com/UncleJunVIP/gabagool v0.0.30/go.mod h1:AwCrrFLlQwwCQo0eVbPPF2dSEKqxOAyGGVS3tKFfIAk=
27 | github.com/UncleJunVIP/gabagool v0.0.31 h1:+Fk+LXD+PLqPGIZSYG/SkauzhmgmnsKtkGo3KranOM8=
28 | github.com/UncleJunVIP/gabagool v0.0.31/go.mod h1:AwCrrFLlQwwCQo0eVbPPF2dSEKqxOAyGGVS3tKFfIAk=
29 | github.com/UncleJunVIP/gabagool v0.0.36 h1:s3KIcNZolgB54h+19ORNlsT8Cnx75krzadQ+CXc5uKM=
30 | github.com/UncleJunVIP/gabagool v0.0.36/go.mod h1:AwCrrFLlQwwCQo0eVbPPF2dSEKqxOAyGGVS3tKFfIAk=
31 | github.com/UncleJunVIP/gabagool v0.0.37 h1:Bd92G60UF6ZwkrdgCcJP6QRkVbPLl+IMR0gRa4Sp0q4=
32 | github.com/UncleJunVIP/gabagool v0.0.37/go.mod h1:AwCrrFLlQwwCQo0eVbPPF2dSEKqxOAyGGVS3tKFfIAk=
33 | github.com/UncleJunVIP/gabagool v0.0.38 h1:w/M3aCuTKxFP7DE1R/H5ei3r0XT0P6DbwmiPjWeVpPA=
34 | github.com/UncleJunVIP/gabagool v0.0.38/go.mod h1:AwCrrFLlQwwCQo0eVbPPF2dSEKqxOAyGGVS3tKFfIAk=
35 | github.com/UncleJunVIP/gabagool v0.0.39 h1:C8bkdfDkllxbuCF+eFfhXCLvIpCUJI8dAofmgIq50VQ=
36 | github.com/UncleJunVIP/gabagool v0.0.39/go.mod h1:AwCrrFLlQwwCQo0eVbPPF2dSEKqxOAyGGVS3tKFfIAk=
37 | github.com/UncleJunVIP/gabagool v0.0.40 h1:II+APZ7ACVGt7b+6XuzNGo0IC8wZqOzx1qRiaHp51Dc=
38 | github.com/UncleJunVIP/gabagool v0.0.40/go.mod h1:AwCrrFLlQwwCQo0eVbPPF2dSEKqxOAyGGVS3tKFfIAk=
39 | github.com/UncleJunVIP/gabagool v0.0.41 h1:u7lacMyorOdjOnXLw7noYViGGmBibjBB0GRPTBldZN0=
40 | github.com/UncleJunVIP/gabagool v0.0.41/go.mod h1:AwCrrFLlQwwCQo0eVbPPF2dSEKqxOAyGGVS3tKFfIAk=
41 | github.com/UncleJunVIP/nextui-pak-shared-functions v1.6.1 h1:hKmgw2V9AaSrEdyhsAk+daW2RLE6Dh/VqZYier2MOmI=
42 | github.com/UncleJunVIP/nextui-pak-shared-functions v1.6.1/go.mod h1:sXHrQmYySc2DVrGkhpoAoqDxb3lq3AS1HRbuHKCLnDE=
43 | github.com/UncleJunVIP/nextui-pak-shared-functions v1.7.1 h1:5gpqVfTz2kr5cuXdAYapX7o8CyW41vsDa2kwT7f3NfE=
44 | github.com/UncleJunVIP/nextui-pak-shared-functions v1.7.1/go.mod h1:sXHrQmYySc2DVrGkhpoAoqDxb3lq3AS1HRbuHKCLnDE=
45 | github.com/activcoding/HTML-Table-to-JSON v0.0.4 h1:6xQvdHFFMHHW8ubDS+xUW/USUx25kZtC6nfVnY8tZQU=
46 | github.com/activcoding/HTML-Table-to-JSON v0.0.4/go.mod h1:xStjYiUrfnpo8937cHqbWh4hHGMSpT0sZ+qjvbCTLA0=
47 | github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
48 | github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
49 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
50 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
51 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
52 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
53 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
54 | github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
55 | github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
56 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
57 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
58 | github.com/kettek/apng v0.0.0-20220823221153-ff692776a607 h1:8tP9cdXzcGX2AvweVVG/lxbI7BSjWbNNUustwJ9dQVA=
59 | github.com/kettek/apng v0.0.0-20220823221153-ff692776a607/go.mod h1:x78/VRQYKuCftMWS0uK5e+F5RJ7S4gSlESRWI0Prl6Q=
60 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
61 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
62 | github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
63 | github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
64 | github.com/patrickhuber/go-types v0.6.0 h1:WYAsdXniinKzQZdxC0wvORwtZX8C670tvguslefRMGI=
65 | github.com/patrickhuber/go-types v0.6.0/go.mod h1:fkCDj9+Mfd2t7x0vb8EQywoa1vhBp+G8AW7M5kz7rnc=
66 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
67 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
68 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
69 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
70 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
71 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
72 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
73 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
74 | github.com/veandco/go-sdl2 v0.4.40 h1:fZv6wC3zz1Xt167P09gazawnpa0KY5LM7JAvKpX9d/U=
75 | github.com/veandco/go-sdl2 v0.4.40/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY=
76 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
77 | go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
78 | go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
79 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
80 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
81 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
82 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
83 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
84 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
85 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
86 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
87 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
88 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
89 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
90 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
91 | golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
92 | golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
93 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
94 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
95 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
96 | golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
97 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
98 | golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
99 | golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
100 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
101 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
102 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
103 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
104 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
105 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
106 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
107 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
108 | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
109 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
110 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
111 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
112 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
113 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
114 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
115 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
116 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
117 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
118 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
119 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
120 | golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
121 | golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
122 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
123 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
124 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
125 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
126 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
127 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
128 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
129 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
130 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
131 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
132 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
133 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
134 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
135 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
136 | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
137 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
138 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
139 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
140 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
141 | golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
142 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
143 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
144 | golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
145 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
146 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
147 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
148 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
149 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
150 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
151 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
152 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
153 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
154 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
155 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
156 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
157 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
158 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
159 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
160 | golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
161 | golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
162 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
163 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
164 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
165 | modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s=
166 | modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
167 | modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
168 | modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
169 | modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8=
170 | modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
171 | modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
172 | modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
173 | modernc.org/libc v1.65.6 h1:OhJUhmuJ6MVZdqL5qmnd0/my46DKGFhSX4WOR7ijfyE=
174 | modernc.org/libc v1.65.6/go.mod h1:MOiGAM9lrMBT9L8xT1nO41qYl5eg9gCp9/kWhz5L7WA=
175 | modernc.org/libc v1.65.8 h1:7PXRJai0TXZ8uNA3srsmYzmTyrLoHImV5QxHeni108Q=
176 | modernc.org/libc v1.65.8/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU=
177 | modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
178 | modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
179 | modernc.org/memory v1.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4=
180 | modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
181 | modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
182 | modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
183 | modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
184 | modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
185 | modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
186 | modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
187 | modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI=
188 | modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=
189 | modernc.org/sqlite v1.37.1 h1:EgHJK/FPoqC+q2YBXg7fUmES37pCHFc97sI7zSayBEs=
190 | modernc.org/sqlite v1.37.1/go.mod h1:XwdRtsE1MpiBcL54+MbKcaDvcuej+IYSMfLN6gSKV8g=
191 | modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
192 | modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
193 | modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
194 | modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
195 | qlova.tech v0.1.1 h1:6M4Eybb3taUTYfceUmBDnV4E9upyrdQMIjjo+namyrI=
196 | qlova.tech v0.1.1/go.mod h1:OTsg7MNcQl+eR/6zDIyrNoCRjjCXdRvfmrcDaKnHAFk=
197 |
--------------------------------------------------------------------------------
/go.work:
--------------------------------------------------------------------------------
1 | go 1.24.2
2 |
3 | use (
4 | ../gabagool
5 | .
6 | )
7 |
--------------------------------------------------------------------------------
/go.work.sum:
--------------------------------------------------------------------------------
1 | github.com/UncleJunVIP/gabagool v0.0.40/go.mod h1:AwCrrFLlQwwCQo0eVbPPF2dSEKqxOAyGGVS3tKFfIAk=
2 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
3 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
4 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
5 | github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
6 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
7 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
8 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
9 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
10 | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 h1:IRJeR9r1pYWsHKTRe/IInb7lYvbBVIqOgsX/u0mbOWY=
11 | golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
12 | golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
13 | golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
14 | golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
15 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
16 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
17 | golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
18 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
19 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
20 |
--------------------------------------------------------------------------------
/launch.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | PAK_DIR="$(dirname "$0")"
3 | cd "$PAK_DIR" || exit 1
4 |
5 | export LD_LIBRARY_PATH=/usr/trimui/lib:$PAK_DIR/resources/lib
6 |
7 | ./pak-store
8 |
--------------------------------------------------------------------------------
/models/constants.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | const (
4 | SplashScreen = "/mnt/SDCARD/Tools/tg5040/Pak Store.pak/resources/splash.png"
5 |
6 | StorefrontJson = "https://pak-store.unclejun.vip/storefront.json"
7 | GitHubRoot = "https://github.com/"
8 | RawGHUC = "https://raw.githubusercontent.com/"
9 | RefMainStub = "/refs/heads/main/"
10 | PakJsonStub = "pak.json"
11 |
12 | PakStoreConfigRoot = "/mnt/SDCARD/.userdata/tg5040/nextui-pak-store"
13 | ToolRoot = "/mnt/SDCARD/Tools/tg5040"
14 | EmulatorRoot = "/mnt/SDCARD/Emus/tg5040"
15 |
16 | InitializationError = "Error Initializing Pak Store! Check Logs. Quitting!"
17 | BlankPresenterString = " "
18 | )
19 |
--------------------------------------------------------------------------------
/models/database.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | type PakInstallation struct {
4 | PakName string `json:"pak_name,omitempty"`
5 | Path string `json:"path,omitempty"`
6 | Version string `json:"version,omitempty"`
7 | RepoURL string `json:"repo_url,omitempty"`
8 | InstalledDate string `json:"installed_date,omitempty"`
9 | }
10 |
--------------------------------------------------------------------------------
/models/menu.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | type MenuItems struct {
4 | Items []string
5 | }
6 |
7 | func (m MenuItems) Values() []string {
8 | return m.Items
9 | }
10 |
--------------------------------------------------------------------------------
/models/pak.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "qlova.tech/sum"
5 | )
6 |
7 | type Pak struct {
8 | StorefrontName string `json:"storefront_name"`
9 | Name string `json:"name"`
10 | Version string `json:"version"`
11 | PakType sum.Int[PakType] `json:"type"`
12 | Description string `json:"description"`
13 | Author string `json:"author"`
14 | RepoURL string `json:"repo_url"`
15 | ReleaseFilename string `json:"release_filename"`
16 | Changelog map[string]string `json:"changelog"`
17 | Scripts Scripts `json:"scripts"`
18 | UpdateIgnore []string `json:"update_ignore"`
19 | Screenshots []string `json:"screenshots"`
20 | Platforms []string `json:"platforms"`
21 | Categories []string `json:"categories"`
22 | LargePak bool `json:"large_pak"`
23 | Disabled bool `json:"disabled"`
24 | }
25 |
26 | type Scripts struct {
27 | PostInstall Script `json:"post_install"`
28 | PostUpdate Script `json:"post_update"`
29 | }
30 |
31 | type Script struct {
32 | Path string `json:"path"`
33 | Args []string `json:"args"`
34 | }
35 |
36 | type PakType struct {
37 | TOOL,
38 | EMU sum.Int[PakType]
39 | }
40 |
41 | var PakTypeMap = map[sum.Int[PakType]]string{
42 | PakTypes.TOOL: "TOOL",
43 | PakTypes.EMU: "EMU",
44 | }
45 |
46 | var PakTypes = sum.Int[PakType]{}.Sum()
47 |
48 | func (p Pak) Value() interface{} {
49 | return p
50 | }
51 |
52 | func (p Pak) HasScripts() bool {
53 | return p.Scripts.PostInstall.Path != "" || p.Scripts.PostUpdate.Path != ""
54 | }
55 |
--------------------------------------------------------------------------------
/models/screens.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import "qlova.tech/sum"
4 |
5 | type ScreenName struct {
6 | MainMenu,
7 | Browse,
8 | PakList,
9 | PakInfo,
10 | DownloadPak,
11 | Updates,
12 | ManageInstalled sum.Int[ScreenName]
13 | }
14 |
15 | var ScreenNames = sum.Int[ScreenName]{}.Sum()
16 |
17 | type Screen interface {
18 | Name() sum.Int[ScreenName]
19 | Draw() (value interface{}, exitCode int, e error)
20 | }
21 |
22 | type ScreenReturn interface {
23 | Value() interface{}
24 | }
25 |
26 | type WrappedString struct {
27 | Contents string
28 | }
29 |
30 | func NewWrappedString(s string) WrappedString {
31 | return WrappedString{Contents: s}
32 | }
33 |
34 | func (s WrappedString) Value() interface{} {
35 | return s.Contents
36 | }
37 |
--------------------------------------------------------------------------------
/models/storefront.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | type Storefront struct {
4 | Name string `json:"name"`
5 | URL string `json:"url"`
6 | Paks []Pak `json:"paks"`
7 | }
8 |
--------------------------------------------------------------------------------
/pak.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Pak Store",
3 | "version": "v1.0.3",
4 | "type": "TOOL",
5 | "description": "A Pak Store in this economy?!",
6 | "author": "K-Wall",
7 | "repo_url": "https://github.com/UncleJunVIP/nextui-pak-store",
8 | "release_filename": "Pak.Store.pak.zip",
9 | "screenshots": [
10 | ".github/resources/screenshots/main_menu.jpg",
11 | ".github/resources/screenshots/browse.jpg",
12 | ".github/resources/screenshots/ports.jpg",
13 | ".github/resources/screenshots/portmaster_1.jpg",
14 | ".github/resources/screenshots/portmaster_2.jpg",
15 | ".github/resources/screenshots/updates.jpg",
16 | ".github/resources/screenshots/mortar_1.jpg",
17 | ".github/resources/screenshots/mortar_2.jpg",
18 | ".github/resources/screenshots/mortar_3.jpg"
19 | ],
20 | "platforms": [
21 | "tg5040"
22 | ],
23 | "changelog": {
24 | "v1.0.3": "Make sure Pak Store exits when updating itself.",
25 | "v1.0.2": "Added a message when device is not connected to Wi-Fi.",
26 | "v1.0.0": "Upgraded the UI to use gabagool, my NextUI Pak UI Library!"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/resources/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UncleJunVIP/nextui-pak-store/0350f3036c346cedceb670b10b86c6914a3c758a/resources/splash.png
--------------------------------------------------------------------------------
/sql/queries.sql:
--------------------------------------------------------------------------------
1 | -- name: ListInstalledPaks :many
2 | SELECT *
3 | FROM installed_paks
4 | ORDER BY name;
5 |
6 | -- name: Install :exec
7 | INSERT INTO installed_paks (display_name, name, version, type, can_uninstall)
8 | VALUES (?, ?, ?, ?, ?);
9 |
10 | -- name: UpdateVersion :exec
11 | UPDATE installed_paks
12 | SET version = ?
13 | WHERE name = ?;
14 |
15 | -- name: Uninstall :exec
16 | DELETE
17 | FROM installed_paks
18 | WHERE name = ?;
--------------------------------------------------------------------------------
/sql/schema.sql:
--------------------------------------------------------------------------------
1 | create table installed_paks
2 | (
3 | name text not null,
4 | display_name text not null,
5 | type text not null,
6 | version text not null,
7 | can_uninstall int not null,
8 | unique (name)
9 | );
10 |
--------------------------------------------------------------------------------
/sqlc.yaml:
--------------------------------------------------------------------------------
1 | version: "2"
2 | sql:
3 | - engine: "sqlite"
4 | queries: "resources/queries.sql"
5 | schema: "resources/schema.sql"
6 | gen:
7 | go:
8 | package: "database"
9 | out: "database"
--------------------------------------------------------------------------------
/state/app_state.go:
--------------------------------------------------------------------------------
1 | package state
2 |
3 | import (
4 | "context"
5 | "github.com/UncleJunVIP/nextui-pak-shared-functions/common"
6 | "github.com/UncleJunVIP/nextui-pak-store/database"
7 | "github.com/UncleJunVIP/nextui-pak-store/models"
8 | "go.uber.org/zap"
9 | "golang.org/x/mod/semver"
10 | "slices"
11 | "strings"
12 | )
13 |
14 | var LastSelectedIndex, LastSelectedPosition int
15 |
16 | type AppState struct {
17 | Storefront models.Storefront
18 | InstalledPaks map[string]database.InstalledPak
19 | AvailablePaks []models.Pak
20 | BrowsePaks map[string]map[string]models.Pak // Sorted by category
21 | UpdatesAvailable []models.Pak
22 | UpdatesAvailableMap map[string]models.Pak
23 | }
24 |
25 | func NewAppState(storefront models.Storefront) AppState {
26 | return refreshAppState(storefront)
27 | }
28 |
29 | func (appState *AppState) Refresh() AppState {
30 | return refreshAppState(appState.Storefront)
31 | }
32 |
33 | func refreshAppState(storefront models.Storefront) AppState {
34 | logger := common.GetLoggerInstance()
35 | ctx := context.Background()
36 |
37 | installed, err := database.DBQ().ListInstalledPaks(ctx)
38 | if err != nil {
39 | //_, _ = cui.ShowMessage(models.InitializationError, "3")
40 | logger.Fatal("Unable to read installed paks table", zap.Error(err))
41 | }
42 |
43 | installedPaksMap := make(map[string]database.InstalledPak)
44 | for _, p := range installed {
45 | installedPaksMap[p.DisplayName] = p
46 | }
47 |
48 | var availablePaks []models.Pak
49 | var updatesAvailable []models.Pak
50 | updatesAvailableMap := make(map[string]models.Pak)
51 | browsePaks := make(map[string]map[string]models.Pak)
52 |
53 | for _, p := range storefront.Paks {
54 | if _, ok := installedPaksMap[p.StorefrontName]; !ok {
55 | availablePaks = append(availablePaks, p)
56 | for _, cat := range p.Categories {
57 | if _, ok := browsePaks[cat]; !ok {
58 | browsePaks[cat] = make(map[string]models.Pak)
59 | }
60 | browsePaks[cat][p.StorefrontName] = p
61 | }
62 | } else if hasUpdate(installedPaksMap[p.StorefrontName].Version, p.Version) {
63 | updatesAvailable = append(updatesAvailable, p)
64 | updatesAvailableMap[p.StorefrontName] = p
65 | }
66 | }
67 |
68 | slices.SortFunc(updatesAvailable, func(a, b models.Pak) int {
69 | return strings.Compare(a.StorefrontName, b.StorefrontName)
70 | })
71 |
72 | delete(installedPaksMap, "Pak Store")
73 |
74 | return AppState{
75 | Storefront: storefront,
76 | InstalledPaks: installedPaksMap,
77 | UpdatesAvailable: updatesAvailable,
78 | UpdatesAvailableMap: updatesAvailableMap,
79 | AvailablePaks: availablePaks,
80 | BrowsePaks: browsePaks,
81 | }
82 | }
83 |
84 | func hasUpdate(installed string, latest string) bool {
85 | if !strings.HasPrefix(installed, "v") {
86 | installed = "v" + installed
87 | }
88 |
89 | if !strings.HasPrefix(latest, "v") {
90 | latest = "v" + latest
91 | }
92 |
93 | return semver.Compare(installed, latest) == -1
94 | }
95 |
--------------------------------------------------------------------------------
/storefront_base.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "NextUI Officially Unofficial",
3 | "url": "https://github.com/UncleJunVIP/nextui-pak-store",
4 | "paks": [
5 | {
6 | "storefront_name": "Pak Store",
7 | "repo_url": "https://github.com/UncleJunVIP/nextui-pak-store",
8 | "categories": []
9 | },
10 | {
11 | "storefront_name": "Mortar",
12 | "repo_url": "https://github.com/UncleJunVIP/Mortar",
13 | "categories": [
14 | "ROM Management"
15 | ]
16 | },
17 | {
18 | "storefront_name": "Game Manager",
19 | "repo_url": "https://github.com/UncleJunVIP/nextui-game-manager",
20 | "categories": [
21 | "ROM Management"
22 | ]
23 | },
24 | {
25 | "storefront_name": "Power Menu",
26 | "repo_url": "https://github.com/UncleJunVIP/nextui-power-menu",
27 | "categories": [
28 | "System"
29 | ]
30 | },
31 | {
32 | "storefront_name": "Screenshot Monitor",
33 | "repo_url": "https://github.com/josegonzalez/minui-screenshot-monitor-pak",
34 | "categories": [
35 | "Media"
36 | ]
37 | },
38 | {
39 | "storefront_name": "Dropbear Server (SSH)",
40 | "repo_url": "https://github.com/josegonzalez/minui-dropbear-server-pak",
41 | "categories": [
42 | "File Management"
43 | ]
44 | },
45 | {
46 | "storefront_name": "SFTPGO Server",
47 | "repo_url": "https://github.com/josegonzalez/minui-sftpgo-server-pak",
48 | "categories": [
49 | "File Management"
50 | ]
51 | },
52 | {
53 | "storefront_name": "Dufs Server",
54 | "repo_url": "https://github.com/josegonzalez/minui-dufs-server-pak",
55 | "categories": [
56 | "File Management"
57 | ]
58 | },
59 | {
60 | "storefront_name": "File Browser",
61 | "repo_url": "https://github.com/josegonzalez/minui-filebrowser-pak",
62 | "categories": [
63 | "File Management"
64 | ]
65 | },
66 | {
67 | "storefront_name": "FTP Server",
68 | "repo_url": "https://github.com/josegonzalez/minui-ftpserver-pak",
69 | "categories": [
70 | "File Management"
71 | ]
72 | },
73 | {
74 | "storefront_name": "Syncthing",
75 | "repo_url": "https://github.com/josegonzalez/minui-syncthing-pak",
76 | "categories": [
77 | "File Management"
78 | ]
79 | },
80 | {
81 | "storefront_name": "USB Mass Storage",
82 | "repo_url": "https://github.com/josegonzalez/trimui-brick-usb-mass-storage-pak",
83 | "categories": [
84 | "File Management"
85 | ]
86 | },
87 | {
88 | "storefront_name": "Developer",
89 | "repo_url": "https://github.com/josegonzalez/minui-developer-pak",
90 | "categories": [
91 | "Developer Tools"
92 | ]
93 | },
94 | {
95 | "storefront_name": "Remote Terminal",
96 | "repo_url": "https://github.com/josegonzalez/minui-remote-terminal-pak",
97 | "categories": [
98 | "Developer Tools"
99 | ]
100 | },
101 | {
102 | "storefront_name": "Report",
103 | "repo_url": "https://github.com/josegonzalez/minui-report-pak",
104 | "categories": [
105 | "Developer Tools"
106 | ]
107 | },
108 | {
109 | "storefront_name": "Terminal",
110 | "repo_url": "https://github.com/josegonzalez/minui-terminal-pak",
111 | "categories": [
112 | "Developer Tools"
113 | ]
114 | },
115 | {
116 | "storefront_name": "Simple Mode",
117 | "repo_url": "https://github.com/josegonzalez/minui-simple-mode-pak",
118 | "categories": [
119 | "Miscellaneous Tools"
120 | ]
121 | },
122 | {
123 | "storefront_name": "Artwork Scraper",
124 | "repo_url": "https://github.com/josegonzalez/minui-artwork-scraper-pak",
125 | "categories": [
126 | "ROM Management",
127 | "Media"
128 | ]
129 | },
130 | {
131 | "storefront_name": "Random Game",
132 | "repo_url": "https://github.com/josegonzalez/minui-random-game-pak",
133 | "categories": [
134 | "Miscellaneous Tools"
135 | ]
136 | },
137 | {
138 | "storefront_name": "PICO-8",
139 | "repo_url": "https://github.com/josegonzalez/minui-pico-8-pak",
140 | "categories": [
141 | "Emulators"
142 | ]
143 | },
144 | {
145 | "storefront_name": "Nintendo DS (DraStic)",
146 | "repo_url": "https://github.com/josegonzalez/minui-nintendo-ds-pak",
147 | "categories": [
148 | "Emulators"
149 | ]
150 | },
151 | {
152 | "storefront_name": "Nintendo 64 (mupen64plus)",
153 | "repo_url": "https://github.com/josegonzalez/minui-n64-pak",
154 | "categories": [
155 | "Emulators"
156 | ]
157 | },
158 | {
159 | "storefront_name": "Dreamcast (Flycast)",
160 | "repo_url": "https://github.com/josegonzalez/minui-dreamcast-pak",
161 | "categories": [
162 | "Emulators"
163 | ]
164 | },
165 | {
166 | "storefront_name": "Moonlight",
167 | "repo_url": "https://github.com/josegonzalez/trimui-brick-moonlight-pak",
168 | "categories": [
169 | "Streaming Game Clients"
170 | ]
171 | },
172 | {
173 | "storefront_name": "Media Player",
174 | "repo_url": "https://github.com/josegonzalez/trimui-brick-media-player-pak",
175 | "categories": [
176 | "Media"
177 | ]
178 | },
179 | {
180 | "storefront_name": "Gallery Pak",
181 | "repo_url": "https://github.com/josegonzalez/minui-gallery-pak",
182 | "categories": [
183 | "Media"
184 | ]
185 | },
186 | {
187 | "storefront_name": "Function Key Editor",
188 | "repo_url": "https://github.com/josegonzalez/trimui-brick-fn-editor-pak",
189 | "categories": [
190 | "Customization"
191 | ]
192 | },
193 | {
194 | "storefront_name": "Theme Manager",
195 | "repo_url": "https://github.com/Leviathanium/NextUI-Theme-Manager",
196 | "categories": [
197 | "Customization"
198 | ]
199 | },
200 | {
201 | "storefront_name": "Portmaster",
202 | "repo_url": "https://github.com/ben16w/minui-portmaster",
203 | "categories": [
204 | "Emulators",
205 | "Ports"
206 | ],
207 | "large_pak": true
208 | },
209 | {
210 | "storefront_name": "PPSSPP",
211 | "repo_url": "https://github.com/ben16w/minui-psp",
212 | "categories": [
213 | "Emulators"
214 | ]
215 | },
216 | {
217 | "storefront_name": "Cloud Backups",
218 | "repo_url": "https://github.com/ben16w/minui-cloud-backups",
219 | "categories": [
220 | "File Management"
221 | ]
222 | },
223 | {
224 | "storefront_name": "Favorites",
225 | "repo_url": "https://github.com/ben16w/minui-favorites",
226 | "categories": [
227 | "ROM Management"
228 | ]
229 | },
230 | {
231 | "storefront_name": "Tailscale",
232 | "repo_url": "https://github.com/ben16w/minui-tailscale",
233 | "categories": [
234 | "Developer Tools",
235 | "Miscellaneous Tools"
236 | ]
237 | },
238 | {
239 | "storefront_name": "Over The Air Updater",
240 | "repo_url": "https://github.com/LanderN/nextui-updater-pak",
241 | "categories": [
242 | "System"
243 | ]
244 | },
245 | {
246 | "storefront_name": "Search Pak",
247 | "repo_url": "https://github.com/laesetuc/minui-search-pak",
248 | "categories": [
249 | "ROM Management"
250 | ]
251 | },
252 | {
253 | "storefront_name": "Collections Editor",
254 | "repo_url": "https://github.com/laesetuc/minui-collections-editor",
255 | "categories": [
256 | "ROM Management"
257 | ]
258 | },
259 | {
260 | "storefront_name": "Dot File Cleaner",
261 | "repo_url": "https://github.com/laesetuc/minui-dotclean-pak",
262 | "categories": [
263 | "System"
264 | ]
265 | },
266 | {
267 | "storefront_name": "ScummVM",
268 | "repo_url": "https://github.com/laesetuc/minui-scummvm",
269 | "categories": [
270 | "Emulators"
271 | ]
272 | }
273 | ]
274 | }
275 |
--------------------------------------------------------------------------------
/taskfile.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | tasks:
4 | all:
5 | cmds:
6 | - task: cleanup
7 | - task: build
8 | - task: package
9 | - task: adb
10 | silent: true
11 |
12 | build:
13 | cmds:
14 | - rm -rf build
15 | - mkdir -p build
16 | - mkdir -p build/lib
17 | - docker buildx build --platform=linux/arm64 -t retro-console-arm64 -f Dockerfile .
18 | silent: true
19 |
20 | package:
21 | cmds:
22 | - docker create --name extract retro-console-arm64 || true
23 | - docker cp extract:/build/pak-store build/pak-store
24 | - docker cp extract:/usr/lib/aarch64-linux-gnu/libSDL2_gfx-1.0.so.0.0.2 build/lib/libSDL2_gfx-1.0.so.0
25 | - rm -rf "build/Pak Store.pak" || true
26 | - mkdir -p "build/Pak Store.pak"
27 | - mkdir -p "build/Pak Store.pak/resources/lib"
28 | - cp build/pak-store launch.sh README.md LICENSE pak.json "build/Pak Store.pak"
29 | - cp resources/splash.png "build/Pak Store.pak/resources"
30 | - cp -R build/lib "build/Pak Store.pak/resources"
31 | silent: true
32 |
33 | cleanup:
34 | cmds:
35 | - docker rm extract || true
36 | silent: true
37 |
38 | adb:
39 | cmds:
40 | - adb shell rm -rf "/mnt/SDCARD/Tools/tg5040/Pak Store.pak" || true
41 | - adb push "build/Pak Store.pak" /mnt/SDCARD/Tools/tg5040
42 | - say Finished deploying Pak Store!
43 | silent: true
44 |
45 | kill:
46 | cmds:
47 | - sshpass -p 'tina' ssh root@192.168.1.210 "kill \$(pidof dlv)" > /dev/null 2>&1 || true
48 | - sshpass -p 'tina' ssh root@192.168.1.210 "kill \$(pidof pak-store)" > /dev/null 2>&1 || true
49 | silent: true
50 |
51 | debug:
52 | cmds:
53 | - sshpass -p 'tina' ssh root@192.168.1.210 "sh -c '/mnt/SDCARD/Developer/bin/dlv attach --headless --listen=:2345 --api-version=2 --accept-multiclient \$(pidof pak-store)'" > /dev/null &
54 | - printf "Press any key to quit debugging...\n"
55 | - read
56 | - task: kill
57 | silent: true
58 |
--------------------------------------------------------------------------------
/ui/browse.go:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import (
4 | gaba "github.com/UncleJunVIP/gabagool/pkg/gabagool"
5 | "github.com/UncleJunVIP/nextui-pak-store/models"
6 | "github.com/UncleJunVIP/nextui-pak-store/state"
7 | "qlova.tech/sum"
8 | "slices"
9 | "strconv"
10 | "strings"
11 | )
12 |
13 | type BrowseScreen struct {
14 | AppState state.AppState
15 | }
16 |
17 | func InitBrowseScreen(appState state.AppState) BrowseScreen {
18 | return BrowseScreen{
19 | AppState: appState,
20 | }
21 | }
22 |
23 | func (bs BrowseScreen) Name() sum.Int[models.ScreenName] {
24 | return models.ScreenNames.Browse
25 | }
26 |
27 | func (bs BrowseScreen) Draw() (selection interface{}, exitCode int, e error) {
28 | var menuItems []gaba.MenuItem
29 |
30 | for cat := range bs.AppState.BrowsePaks {
31 | menuItems = append(menuItems, gaba.MenuItem{
32 | Text: cat + " (" + strconv.Itoa(len(bs.AppState.BrowsePaks[cat])) + ")",
33 | Selected: false,
34 | Focused: false,
35 | Metadata: cat,
36 | })
37 | }
38 |
39 | slices.SortFunc(menuItems, func(a, b gaba.MenuItem) int {
40 | return strings.Compare(a.Text, b.Text)
41 | })
42 |
43 | options := gaba.DefaultListOptions("Browse Paks", menuItems)
44 | options.EnableAction = true
45 | options.FooterHelpItems = []gaba.FooterHelpItem{
46 | {ButtonName: "B", HelpText: "Back"},
47 | {ButtonName: "A", HelpText: "Select"},
48 | }
49 |
50 | sel, err := gaba.List(options)
51 | if err != nil {
52 | return nil, -1, err
53 | }
54 |
55 | if sel.IsNone() || sel.Unwrap().SelectedIndex == -1 {
56 | return nil, 2, nil
57 | }
58 |
59 | return sel.Unwrap().SelectedItem.Metadata, 0, nil
60 | }
61 |
--------------------------------------------------------------------------------
/ui/main_menu.go:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import (
4 | "fmt"
5 | gaba "github.com/UncleJunVIP/gabagool/pkg/gabagool"
6 | "github.com/UncleJunVIP/nextui-pak-store/models"
7 | "github.com/UncleJunVIP/nextui-pak-store/state"
8 | "qlova.tech/sum"
9 | "strings"
10 | )
11 |
12 | type MainMenu struct {
13 | AppState state.AppState
14 | }
15 |
16 | func InitMainMenu(appState state.AppState) MainMenu {
17 | return MainMenu{
18 | AppState: appState,
19 | }
20 | }
21 |
22 | func (m MainMenu) Name() sum.Int[models.ScreenName] {
23 | return models.ScreenNames.MainMenu
24 | }
25 |
26 | func (m MainMenu) Draw() (selection interface{}, exitCode int, e error) {
27 | title := "Pak Store"
28 |
29 | var menuItems []gaba.MenuItem
30 |
31 | if len(m.AppState.UpdatesAvailable) > 0 {
32 | menuItems = append(menuItems, gaba.MenuItem{
33 | Text: fmt.Sprintf("Available Updates (%d)", len(m.AppState.UpdatesAvailable)),
34 | Selected: false,
35 | Focused: false,
36 | Metadata: "Available Updates",
37 | })
38 | }
39 |
40 | if len(m.AppState.BrowsePaks) > 0 {
41 | menuItems = append(menuItems, gaba.MenuItem{
42 | Text: fmt.Sprintf("Browse (%d)", len(m.AppState.AvailablePaks)),
43 | Selected: false,
44 | Focused: false,
45 | Metadata: "Browse",
46 | })
47 | }
48 |
49 | if len(m.AppState.InstalledPaks) > 0 {
50 | menuItems = append(menuItems, gaba.MenuItem{
51 | Text: fmt.Sprintf("Manage Installed (%d)", len(m.AppState.InstalledPaks)),
52 | Selected: false,
53 | Focused: false,
54 | Metadata: "Manage Installed",
55 | })
56 | }
57 |
58 | options := gaba.DefaultListOptions(title, menuItems)
59 | options.EnableAction = true
60 | options.FooterHelpItems = []gaba.FooterHelpItem{
61 | {ButtonName: "B", HelpText: "Quit"},
62 | {ButtonName: "A", HelpText: "Select"},
63 | }
64 |
65 | sel, err := gaba.List(options)
66 | if err != nil {
67 | return nil, -1, err
68 | }
69 |
70 | if sel.IsNone() || sel.Unwrap().SelectedIndex == -1 {
71 | return nil, 2, nil
72 | }
73 |
74 | trimmedCount := strings.Split(sel.Unwrap().SelectedItem.Text, " (")[0] // TODO clean this up with regex
75 |
76 | return trimmedCount, 0, nil
77 | }
78 |
--------------------------------------------------------------------------------
/ui/manage.go:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | gaba "github.com/UncleJunVIP/gabagool/pkg/gabagool"
7 | "github.com/UncleJunVIP/nextui-pak-shared-functions/common"
8 | "github.com/UncleJunVIP/nextui-pak-store/database"
9 | "github.com/UncleJunVIP/nextui-pak-store/models"
10 | "github.com/UncleJunVIP/nextui-pak-store/state"
11 | "go.uber.org/zap"
12 | "os"
13 | "path/filepath"
14 | "qlova.tech/sum"
15 | "slices"
16 | "strings"
17 | "time"
18 | )
19 |
20 | type ManageInstalledScreen struct {
21 | AppState state.AppState
22 | }
23 |
24 | func InitManageInstalledScreen(appState state.AppState) ManageInstalledScreen {
25 | return ManageInstalledScreen{
26 | AppState: appState,
27 | }
28 | }
29 |
30 | func (mis ManageInstalledScreen) Name() sum.Int[models.ScreenName] {
31 | return models.ScreenNames.ManageInstalled
32 | }
33 |
34 | func (mis ManageInstalledScreen) Draw() (selection interface{}, exitCode int, e error) {
35 | if len(mis.AppState.InstalledPaks) == 0 {
36 | return nil, 2, nil
37 | }
38 |
39 | logger := common.GetLoggerInstance()
40 |
41 | var menuItems []gaba.MenuItem
42 |
43 | for _, pak := range mis.AppState.InstalledPaks {
44 | menuItems = append(menuItems, gaba.MenuItem{
45 | Text: pak.DisplayName,
46 | Selected: false,
47 | Focused: false,
48 | Metadata: pak,
49 | })
50 | }
51 |
52 | slices.SortFunc(menuItems, func(a, b gaba.MenuItem) int {
53 | return strings.Compare(a.Text, b.Text)
54 | })
55 |
56 | options := gaba.DefaultListOptions("Manage Installed Paks", menuItems)
57 | options.EnableAction = true
58 | options.FooterHelpItems = []gaba.FooterHelpItem{
59 | {ButtonName: "B", HelpText: "Back"},
60 | {ButtonName: "A", HelpText: "Uninstall"},
61 | }
62 |
63 | sel, err := gaba.List(options)
64 | if err != nil {
65 | return nil, -1, err
66 | }
67 |
68 | if sel.IsNone() || sel.Unwrap().SelectedIndex == -1 {
69 | return nil, 2, nil
70 | }
71 |
72 | selectedPak := sel.Unwrap().SelectedItem.Metadata.(database.InstalledPak)
73 |
74 | confirm, err := gaba.ConfirmationMessage(fmt.Sprintf("Are you sure that you want to uninstall\n %s?", selectedPak.DisplayName),
75 | []gaba.FooterHelpItem{
76 | {ButtonName: "B", HelpText: "Nevermind"},
77 | {ButtonName: "X", HelpText: "Yes"},
78 | }, gaba.MessageOptions{
79 | ConfirmButton: gaba.ButtonX,
80 | })
81 |
82 | if err != nil {
83 | return nil, -1, err
84 | }
85 |
86 | if confirm.IsNone() {
87 | return nil, 12, nil
88 | }
89 |
90 | _, err = gaba.ProcessMessage(fmt.Sprintf("%s %s...", "Uninstalling", selectedPak.Name), gaba.ProcessMessageOptions{}, func() (interface{}, error) {
91 | pakLocation := ""
92 |
93 | if selectedPak.Type == "TOOL" {
94 | pakLocation = filepath.Join(models.ToolRoot, selectedPak.Name+".pak")
95 | } else if selectedPak.Type == "EMU" {
96 | pakLocation = filepath.Join(models.EmulatorRoot, selectedPak.Name+".pak")
97 | }
98 |
99 | err = os.RemoveAll(pakLocation)
100 |
101 | time.Sleep(1750 * time.Millisecond)
102 |
103 | return nil, err
104 | })
105 |
106 | if err != nil {
107 | gaba.ProcessMessage(fmt.Sprintf("Unable to uninstall %s", selectedPak.Name), gaba.ProcessMessageOptions{}, func() (interface{}, error) {
108 | time.Sleep(3 * time.Second)
109 | return nil, nil
110 | })
111 | logger.Error("Unable to remove pak", zap.Error(err))
112 | }
113 |
114 | ctx := context.Background()
115 | err = database.DBQ().Uninstall(ctx, selectedPak.Name)
116 | if err != nil {
117 | // TODO wtf do I do here?
118 | }
119 |
120 | return nil, 0, nil
121 | }
122 |
--------------------------------------------------------------------------------
/ui/pak_info.go:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | gaba "github.com/UncleJunVIP/gabagool/pkg/gabagool"
7 | "github.com/UncleJunVIP/nextui-pak-shared-functions/common"
8 | "github.com/UncleJunVIP/nextui-pak-store/database"
9 | "github.com/UncleJunVIP/nextui-pak-store/models"
10 | "github.com/UncleJunVIP/nextui-pak-store/utils"
11 | "go.uber.org/zap"
12 | "qlova.tech/sum"
13 | "strings"
14 | "sync"
15 | "time"
16 | )
17 |
18 | type PakInfoScreen struct {
19 | Pak models.Pak
20 | Category string
21 | IsUpdate bool
22 | }
23 |
24 | func InitPakInfoScreen(pak models.Pak, category string, isUpdate bool) PakInfoScreen {
25 | return PakInfoScreen{
26 | Pak: pak,
27 | Category: category,
28 | IsUpdate: isUpdate,
29 | }
30 | }
31 |
32 | func (pi PakInfoScreen) Name() sum.Int[models.ScreenName] {
33 | return models.ScreenNames.PakInfo
34 | }
35 |
36 | func (pi PakInfoScreen) Draw() (selection interface{}, exitCode int, e error) {
37 | logger := common.GetLoggerInstance()
38 |
39 | // Pre-allocate the screenshots slice with the correct size
40 | screenshots := make([]string, len(pi.Pak.Screenshots))
41 |
42 | // Create a semaphore to limit concurrent downloads
43 | const maxConcurrentDownloads = 4 // Adjust based on your network and API capabilities
44 | sem := make(chan struct{}, maxConcurrentDownloads)
45 |
46 | var wg sync.WaitGroup
47 |
48 | for i, s := range pi.Pak.Screenshots {
49 | wg.Add(1)
50 | go func(index int, screenshot string) {
51 | sem <- struct{}{}
52 | defer func() {
53 | <-sem
54 | wg.Done()
55 | }()
56 |
57 | uri := pi.Pak.RepoURL + models.RefMainStub + screenshot
58 | uri = strings.ReplaceAll(uri, models.GitHubRoot, models.RawGHUC)
59 |
60 | downloadedScreenshot, err := utils.DownloadTempFile(uri)
61 | if err == nil {
62 | screenshots[index] = downloadedScreenshot
63 | } else {
64 | logger.Error("Failed to download screenshot",
65 | zap.Error(err),
66 | zap.String("uri", uri),
67 | zap.Int("attempt", 1))
68 |
69 | downloadedScreenshot, err = utils.DownloadTempFile(uri)
70 | if err == nil {
71 | screenshots[index] = downloadedScreenshot
72 | } else {
73 | logger.Error("Failed to download screenshot after retry",
74 | zap.Error(err),
75 | zap.String("uri", uri))
76 | }
77 | }
78 | }(i, s)
79 | }
80 |
81 | // Wait for all downloads to complete
82 | wg.Wait()
83 |
84 | // Remove any empty strings (failed downloads) from the result
85 | filteredScreenshots := make([]string, 0, len(screenshots))
86 | for _, s := range screenshots {
87 | if s != "" {
88 | filteredScreenshots = append(filteredScreenshots, s)
89 | }
90 | }
91 | screenshots = filteredScreenshots
92 |
93 | // Rest of the function remains unchanged
94 | var sections []gaba.Section
95 |
96 | if _, ok := pi.Pak.Changelog[pi.Pak.Version]; ok && pi.IsUpdate {
97 | sections = append(sections,
98 | gaba.NewDescriptionSection(
99 | fmt.Sprintf("What's new in %s?", pi.Pak.Version),
100 | pi.Pak.Changelog[pi.Pak.Version],
101 | ))
102 | }
103 |
104 | if pi.Pak.Description != "" {
105 | sections = append(sections, gaba.NewDescriptionSection(
106 | "Description",
107 | pi.Pak.Description,
108 | ))
109 | }
110 |
111 | if len(screenshots) > 0 {
112 | sections = append(sections, gaba.NewSlideshowSection(
113 | "Screenshots",
114 | screenshots,
115 | int32(float64(gaba.GetWindow().Width)/1.2),
116 | int32(float64(gaba.GetWindow().Height)/1.2),
117 | ))
118 | }
119 |
120 | sections = append(sections, gaba.NewInfoSection(
121 | "Pak Info",
122 | []gaba.MetadataItem{
123 | {Label: "Author", Value: pi.Pak.Author},
124 | {Label: "Version", Value: pi.Pak.Version},
125 | },
126 | ))
127 |
128 | qrcode, err := utils.CreateTempQRCode(pi.Pak.RepoURL, 256)
129 | if err == nil {
130 | sections = append(sections, gaba.NewImageSection(
131 | "Pak Repository",
132 | qrcode,
133 | int32(256),
134 | int32(256),
135 | gaba.AlignCenter,
136 | ))
137 |
138 | } else {
139 | logger.Error("Unable to generate QR code", zap.Error(err))
140 | }
141 |
142 | options := gaba.DefaultInfoScreenOptions()
143 | options.Sections = sections
144 | options.ShowThemeBackground = false
145 | options.ConfirmButton = gaba.ButtonX
146 |
147 | confirmLabel := "Install"
148 |
149 | if pi.IsUpdate {
150 | confirmLabel = "Update"
151 | }
152 |
153 | footerItems := []gaba.FooterHelpItem{
154 | {ButtonName: "B", HelpText: "Back"},
155 | {ButtonName: "X", HelpText: confirmLabel},
156 | }
157 |
158 | sel, err := gaba.DetailScreen(pi.Pak.StorefrontName, options, footerItems)
159 | if err != nil {
160 | logger.Error("Unable to display pak info screen", zap.Error(err))
161 | return pi.IsUpdate, -1, err
162 | }
163 |
164 | if sel.IsNone() {
165 | return pi.IsUpdate, 2, nil
166 | }
167 |
168 | action := "Installing"
169 | if pi.IsUpdate {
170 | action = "Updating"
171 | }
172 |
173 | tmp, completed, err := utils.DownloadPakArchive(pi.Pak, action)
174 | if err != nil {
175 |
176 | if err.Error() == "download cancelled by user" {
177 | return pi.IsUpdate, 86, nil
178 | }
179 |
180 | logger.Error("Unable to download pak archive", zap.Error(err))
181 | return pi.IsUpdate, -1, err
182 | } else if !completed {
183 | return pi.IsUpdate, 86, nil
184 | }
185 |
186 | err = utils.UnzipPakArchive(pi.Pak, tmp)
187 | if err != nil {
188 | return pi.IsUpdate, -1, err
189 | }
190 |
191 | if pi.Pak.HasScripts() {
192 | if !pi.IsUpdate {
193 |
194 | }
195 | }
196 |
197 | if !pi.IsUpdate {
198 | info := database.InstallParams{
199 | DisplayName: pi.Pak.StorefrontName,
200 | Name: pi.Pak.Name,
201 | Version: pi.Pak.Version,
202 | Type: models.PakTypeMap[pi.Pak.PakType],
203 | CanUninstall: int64(1),
204 | }
205 | database.DBQ().Install(context.Background(), info)
206 | } else {
207 | update := database.UpdateVersionParams{
208 | Name: pi.Pak.Name,
209 | Version: pi.Pak.Version,
210 | }
211 | database.DBQ().UpdateVersion(context.Background(), update)
212 | }
213 |
214 | action = "Installed"
215 | if pi.IsUpdate {
216 | action = "Updated"
217 | }
218 |
219 | if pi.Pak.Name == "Pak Store" {
220 | return pi.IsUpdate, 23, nil
221 | }
222 |
223 | gaba.ProcessMessage(fmt.Sprintf("%s %s!", pi.Pak.StorefrontName, action), gaba.ProcessMessageOptions{}, func() (interface{}, error) {
224 | time.Sleep(3 * time.Second)
225 | return nil, nil
226 | })
227 |
228 | return pi.IsUpdate, 0, nil
229 | }
230 |
--------------------------------------------------------------------------------
/ui/pak_list.go:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import (
4 | gaba "github.com/UncleJunVIP/gabagool/pkg/gabagool"
5 | "github.com/UncleJunVIP/nextui-pak-store/models"
6 | "github.com/UncleJunVIP/nextui-pak-store/state"
7 | "qlova.tech/sum"
8 | "slices"
9 | "strings"
10 | )
11 |
12 | type PakList struct {
13 | AppState state.AppState
14 | Category string
15 | }
16 |
17 | func InitPakList(appState state.AppState, category string) PakList {
18 | return PakList{
19 | AppState: appState,
20 | Category: category,
21 | }
22 | }
23 |
24 | func (pl PakList) Name() sum.Int[models.ScreenName] {
25 | return models.ScreenNames.PakList
26 | }
27 |
28 | func (pl PakList) Draw() (selection interface{}, exitCode int, e error) {
29 | var menuItems []gaba.MenuItem
30 | for _, p := range pl.AppState.BrowsePaks[pl.Category] {
31 | menuItems = append(menuItems, gaba.MenuItem{
32 | Text: p.StorefrontName,
33 | Selected: false,
34 | Focused: false,
35 | Metadata: p,
36 | })
37 | }
38 |
39 | slices.SortFunc(menuItems, func(a, b gaba.MenuItem) int {
40 | return strings.Compare(a.Text, b.Text)
41 | })
42 |
43 | options := gaba.DefaultListOptions(pl.Category, menuItems)
44 |
45 | selectedIndex := state.LastSelectedIndex
46 |
47 | options.SelectedIndex = selectedIndex
48 | options.VisibleStartIndex = max(0, state.LastSelectedIndex-state.LastSelectedPosition)
49 | options.EnableAction = true
50 | options.FooterHelpItems = []gaba.FooterHelpItem{
51 | {ButtonName: "B", HelpText: "Back"},
52 | {ButtonName: "A", HelpText: "View"},
53 | }
54 |
55 | sel, err := gaba.List(options)
56 | if err != nil {
57 | return nil, -1, err
58 | }
59 |
60 | if sel.IsNone() || sel.Unwrap().SelectedIndex == -1 {
61 | return nil, 2, nil
62 | }
63 |
64 | state.LastSelectedIndex = sel.Unwrap().SelectedIndex
65 | state.LastSelectedPosition = sel.Unwrap().VisiblePosition
66 |
67 | return sel.Unwrap().SelectedItem.Metadata, 0, nil
68 | }
69 |
--------------------------------------------------------------------------------
/ui/updates.go:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import (
4 | gaba "github.com/UncleJunVIP/gabagool/pkg/gabagool"
5 | "github.com/UncleJunVIP/nextui-pak-store/models"
6 | "github.com/UncleJunVIP/nextui-pak-store/state"
7 | "qlova.tech/sum"
8 | "slices"
9 | "strings"
10 | )
11 |
12 | type UpdatesScreen struct {
13 | AppState state.AppState
14 | }
15 |
16 | func InitUpdatesScreen(appState state.AppState) UpdatesScreen {
17 | return UpdatesScreen{
18 | AppState: appState,
19 | }
20 | }
21 |
22 | func (us UpdatesScreen) Name() sum.Int[models.ScreenName] {
23 | return models.ScreenNames.Updates
24 | }
25 |
26 | func (us UpdatesScreen) Draw() (selection interface{}, exitCode int, e error) {
27 | if len(us.AppState.UpdatesAvailable) == 0 {
28 | return nil, 2, nil
29 | }
30 |
31 | var menuItems []gaba.MenuItem
32 |
33 | for _, pak := range us.AppState.UpdatesAvailable {
34 | menuItems = append(menuItems, gaba.MenuItem{
35 | Text: pak.StorefrontName,
36 | Selected: false,
37 | Focused: false,
38 | Metadata: pak,
39 | })
40 | }
41 |
42 | slices.SortFunc(menuItems, func(a, b gaba.MenuItem) int {
43 | return strings.Compare(a.Text, b.Text)
44 | })
45 |
46 | options := gaba.DefaultListOptions("Available Pak Updates", menuItems)
47 | options.EnableAction = true
48 | options.FooterHelpItems = []gaba.FooterHelpItem{
49 | {ButtonName: "B", HelpText: "Back"},
50 | {ButtonName: "A", HelpText: "View"},
51 | }
52 |
53 | sel, err := gaba.List(options)
54 | if err != nil {
55 | return nil, -1, err
56 | }
57 |
58 | if sel.IsNone() || sel.Unwrap().SelectedIndex == -1 {
59 | return nil, 2, nil
60 | }
61 |
62 | return sel.Unwrap().SelectedItem.Metadata.(models.Pak), 0, nil
63 | }
64 |
--------------------------------------------------------------------------------
/utils/functions.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "archive/zip"
5 | "bytes"
6 | "encoding/json"
7 | "fmt"
8 | gaba "github.com/UncleJunVIP/gabagool/pkg/gabagool"
9 | "github.com/UncleJunVIP/nextui-pak-shared-functions/common"
10 | "github.com/UncleJunVIP/nextui-pak-store/models"
11 | "github.com/skip2/go-qrcode"
12 | "go.uber.org/zap"
13 | "image/color"
14 | "io"
15 | "net"
16 | "net/http"
17 | "os"
18 | "os/exec"
19 | "path/filepath"
20 | "strings"
21 | "time"
22 | )
23 |
24 | func FetchStorefront(url string) (models.Storefront, error) {
25 | logger := common.GetLoggerInstance()
26 |
27 | var data []byte
28 | var err error
29 |
30 | if os.Getenv("ENVIRONMENT") == "DEV" {
31 | data, err = os.ReadFile("storefront.json")
32 | if err != nil {
33 | return models.Storefront{}, fmt.Errorf("failed to read local storefront.json", err)
34 | }
35 | } else {
36 | data, err = fetch(url)
37 | if err != nil {
38 | return models.Storefront{}, err
39 | }
40 | }
41 |
42 | var sf models.Storefront
43 | if err := json.Unmarshal(data, &sf); err != nil {
44 | return models.Storefront{}, err
45 | }
46 |
47 | logger.Info("Fetched storefront", zap.String("name", sf.Name))
48 |
49 | return sf, nil
50 | }
51 |
52 | func ParseJSONFile(filePath string, out *models.Pak) error {
53 | file, err := os.Open(filePath)
54 | if err != nil {
55 | return fmt.Errorf("failed to open file: %w", err)
56 | }
57 | defer file.Close()
58 |
59 | data, err := io.ReadAll(file)
60 | if err != nil {
61 | return fmt.Errorf("failed to read file: %w", err)
62 | }
63 |
64 | if err := json.Unmarshal(data, out); err != nil {
65 | return fmt.Errorf("failed to unmarshal JSON: %w", err)
66 | }
67 |
68 | return nil
69 | }
70 |
71 | func DownloadPakArchive(pak models.Pak, action string) (tempFile string, completed bool, error error) {
72 | logger := common.GetLoggerInstance()
73 |
74 | releasesStub := fmt.Sprintf("/releases/download/%s/", pak.Version)
75 | dl := pak.RepoURL + releasesStub + pak.ReleaseFilename
76 | tmp := filepath.Join("/tmp", pak.ReleaseFilename)
77 |
78 | message := ""
79 |
80 | if action == "Updating" {
81 | message = fmt.Sprintf("%s %s to %s...", action, pak.StorefrontName, pak.Version)
82 | } else {
83 | message = fmt.Sprintf("%s %s %s...", action, pak.StorefrontName, pak.Version)
84 | }
85 |
86 | res, err := gaba.DownloadManager([]gaba.Download{{
87 | URL: dl,
88 | Location: tmp,
89 | DisplayName: message,
90 | }}, make(map[string]string))
91 |
92 | if err == nil && len(res.Errors) > 0 {
93 | err = res.Errors[0]
94 | }
95 |
96 | if err != nil {
97 | logger.Error("Error downloading", zap.Error(err))
98 | return "", false, err
99 | } else if res.Cancelled {
100 | return "", false, nil
101 | }
102 |
103 | return tmp, true, nil
104 | }
105 |
106 | func RunScript(script models.Script, scriptName string) error {
107 | logger := common.GetLoggerInstance()
108 |
109 | if script.Path == "" {
110 | logger.Info("No script to run")
111 | return nil
112 | }
113 |
114 | _, err := gaba.ProcessMessage(fmt.Sprintf("%s %s %s...", "Running", scriptName, "Script"), gaba.ProcessMessageOptions{}, func() (interface{}, error) {
115 | logger.Info("Running script", zap.String("path", script.Path), zap.Strings("args", script.Args))
116 |
117 | cmd := exec.Command(script.Path, script.Args...)
118 |
119 | var stdout, stderr bytes.Buffer
120 | cmd.Stdout = &stdout
121 | cmd.Stderr = &stderr
122 |
123 | err := cmd.Run()
124 | if err != nil {
125 | logger.Error("Failed to execute script",
126 | zap.String("path", script.Path),
127 | zap.Strings("args", script.Args),
128 | zap.String("stderr", stderr.String()),
129 | zap.Error(err))
130 | return nil, fmt.Errorf("failed to execute script %s: %w", script.Path, err)
131 | }
132 |
133 | if cmd.ProcessState.ExitCode() != 0 {
134 | logger.Error("Script returned non-zero exit code",
135 | zap.String("path", script.Path),
136 | zap.Strings("args", script.Args),
137 | zap.Int("exitCode", cmd.ProcessState.ExitCode()),
138 | zap.String("stderr", stderr.String()))
139 | return nil, fmt.Errorf("script %s exited with code %d: %s",
140 | script.Path, cmd.ProcessState.ExitCode(), stderr.String())
141 | }
142 |
143 | logger.Info("Script executed successfully",
144 | zap.String("path", script.Path),
145 | zap.Strings("args", script.Args),
146 | zap.String("stdout", stdout.String()))
147 |
148 | return nil, nil
149 | })
150 |
151 | return err
152 | }
153 |
154 | func UnzipPakArchive(pak models.Pak, tmp string) error {
155 | logger := common.GetLoggerInstance()
156 |
157 | pakDestination := ""
158 |
159 | if pak.PakType == models.PakTypes.TOOL {
160 | pakDestination = filepath.Join(models.ToolRoot, pak.Name+".pak")
161 | } else if pak.PakType == models.PakTypes.EMU {
162 | pakDestination = filepath.Join(models.EmulatorRoot, pak.Name+".pak")
163 | }
164 |
165 | _, err := gaba.ProcessMessage(fmt.Sprintf("%s %s...", "Unzipping", pak.StorefrontName), gaba.ProcessMessageOptions{}, func() (interface{}, error) {
166 | err := Unzip(tmp, pakDestination, pak, false)
167 | if err != nil {
168 | return nil, err
169 | }
170 |
171 | return nil, nil
172 | })
173 |
174 | if err != nil {
175 | gaba.ProcessMessage(fmt.Sprintf("Unable to unzip %s", pak.StorefrontName), gaba.ProcessMessageOptions{}, func() (interface{}, error) {
176 | time.Sleep(3 * time.Second)
177 | return nil, nil
178 | })
179 | logger.Error("Unable to unzip pak", zap.Error(err))
180 | return err
181 | }
182 |
183 | return nil
184 | }
185 |
186 | func fetch(url string) ([]byte, error) {
187 | resp, err := http.Get(url)
188 | if err != nil {
189 | return nil, err
190 | }
191 | defer resp.Body.Close()
192 |
193 | if resp.StatusCode != http.StatusOK {
194 | return nil, fmt.Errorf("HTTP request failed with status code: %d", resp.StatusCode)
195 | }
196 |
197 | return io.ReadAll(resp.Body)
198 | }
199 |
200 | func DownloadTempFile(url string) (string, error) {
201 | resp, err := http.Get(url)
202 | if err != nil {
203 | return "", err
204 | }
205 | defer resp.Body.Close()
206 |
207 | if resp.StatusCode != http.StatusOK {
208 | return "", fmt.Errorf("bad status: %s", resp.Status)
209 | } else if resp.ContentLength <= 0 {
210 | return "", fmt.Errorf("empty response")
211 | }
212 |
213 | tempFile, err := os.CreateTemp("", "download-*")
214 | if err != nil {
215 | return "", err
216 | }
217 | defer tempFile.Close()
218 |
219 | _, err = io.Copy(tempFile, resp.Body)
220 | if err != nil {
221 | return "", err
222 | }
223 |
224 | return tempFile.Name(), nil
225 | }
226 |
227 | func CreateTempQRCode(content string, size int) (string, error) {
228 | qr, err := qrcode.New(content, qrcode.Medium)
229 |
230 | if err != nil {
231 | return "", err
232 | }
233 |
234 | qr.BackgroundColor = color.Black
235 | qr.ForegroundColor = color.White
236 | qr.DisableBorder = true
237 |
238 | tempFile, err := os.CreateTemp("", "qrcode-*")
239 |
240 | err = qr.Write(size, tempFile)
241 |
242 | if err != nil {
243 | return "", err
244 | }
245 | defer tempFile.Close()
246 |
247 | return tempFile.Name(), err
248 | }
249 |
250 | func Unzip(src, dest string, pak models.Pak, isUpdate bool) error {
251 | r, err := zip.OpenReader(src)
252 | if err != nil {
253 | return err
254 | }
255 | defer func() {
256 | if err := r.Close(); err != nil {
257 | panic(err)
258 | }
259 | }()
260 |
261 | err = os.MkdirAll(dest, 0755)
262 | if err != nil {
263 | return err
264 | }
265 |
266 | extractAndWriteFile := func(f *zip.File) error {
267 | if isUpdate && ShouldIgnoreFile(f.Name, pak) {
268 | return nil
269 | }
270 |
271 | rc, err := f.Open()
272 | if err != nil {
273 | return err
274 | }
275 | defer func() {
276 | if err := rc.Close(); err != nil {
277 | panic(err)
278 | }
279 | }()
280 |
281 | path := filepath.Join(dest, f.Name)
282 |
283 | // Check for ZipSlip (Directory traversal)
284 | if !strings.HasPrefix(path, filepath.Clean(dest)+string(os.PathSeparator)) {
285 | return fmt.Errorf("illegal file path: %s", path)
286 | }
287 |
288 | if f.FileInfo().IsDir() {
289 | err := os.MkdirAll(path, f.Mode())
290 | if err != nil {
291 | return err
292 | }
293 | } else {
294 | err := os.MkdirAll(filepath.Dir(path), f.Mode())
295 | if err != nil {
296 | return err
297 | }
298 |
299 | // Use a temporary file to avoid ETXTBSY error
300 | tempPath := path + ".tmp"
301 | tempFile, err := os.OpenFile(tempPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
302 | if err != nil {
303 | return err
304 | }
305 |
306 | _, err = io.Copy(tempFile, rc)
307 | tempFile.Close() // Close the file before attempting to rename it
308 |
309 | if err != nil {
310 | os.Remove(tempPath) // Clean up on error
311 | return err
312 | }
313 |
314 | // Now rename the temporary file to the target path
315 | err = os.Rename(tempPath, path)
316 | if err != nil {
317 | os.Remove(tempPath) // Clean up on error
318 | return err
319 | }
320 | }
321 | return nil
322 | }
323 |
324 | for _, f := range r.File {
325 | err := extractAndWriteFile(f)
326 | if err != nil {
327 | return err
328 | }
329 | }
330 |
331 | return nil
332 | }
333 |
334 | func ShouldIgnoreFile(filePath string, pak models.Pak) bool {
335 | for _, ignorePattern := range pak.UpdateIgnore {
336 | match, err := filepath.Match(ignorePattern, filePath)
337 | if err == nil && match {
338 | return true
339 | }
340 |
341 | parts := strings.Split(filePath, string(os.PathSeparator))
342 | for i := 0; i < len(parts); i++ {
343 | if i > 0 && strings.HasSuffix(parts[i-1], ".pak") {
344 | break
345 | }
346 |
347 | partialPath := strings.Join(parts[:i+1], string(os.PathSeparator))
348 | match, err := filepath.Match(ignorePattern, partialPath)
349 | if err == nil && match {
350 | return true
351 | }
352 | }
353 | }
354 |
355 | return false
356 | }
357 |
358 | func IsConnectedToInternet() bool {
359 | timeout := 5 * time.Second
360 | _, err := net.DialTimeout("tcp", "8.8.8.8:53", timeout)
361 | return err == nil
362 | }
363 |
--------------------------------------------------------------------------------