├── .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 | Mortar wordmark 3 | 4 | ![GitHub License](https://img.shields.io/github/license/UncleJunVip/nextui-pak-store?style=for-the-badge) 5 | ![GitHub Release](https://img.shields.io/github/v/release/UncleJunVIP/nextui-pak-store?sort=semver&style=for-the-badge) 6 | ![GitHub Repo stars](https://img.shields.io/github/stars/UncleJunVip/nextui-pak-store?style=for-the-badge) 7 | ![GitHub Downloads (specific asset, all releases)](https://img.shields.io/github/downloads/UncleJunVIP/nextui-pak-store/Pak.Store.pak.zip?style=for-the-badge&label=Downloads) 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 | --------------------------------------------------------------------------------