├── .gitignore ├── README.md ├── bundler.json ├── cmd.png ├── console.go ├── db ├── localSwitchFilesDB.go ├── persistentDB.go ├── switchTitlesDB.go └── utils.go ├── dlc_ui.png ├── fileio └── splitFileUtil.go ├── go.mod ├── go.sum ├── gui.go ├── library_ui.png ├── main.go ├── process ├── incompleteTitleProcessor.go ├── organizefolderStructure.go └── organizefolderStructure_test.go ├── prod.keys ├── resources ├── app │ ├── app.css │ ├── app.html │ ├── app.js │ └── lib │ │ ├── css │ │ ├── bootstrap │ │ │ └── bootstrap.min.css │ │ ├── dark │ │ │ ├── toggle-bootstrap-dark.min.css │ │ │ └── toggle-bootstrap.min.css │ │ └── tabulator │ │ │ ├── bootstrap │ │ │ ├── tabulator_bootstrap.css │ │ │ ├── tabulator_bootstrap.min.css │ │ │ ├── tabulator_bootstrap.min.css.map │ │ │ ├── tabulator_bootstrap4.css │ │ │ ├── tabulator_bootstrap4.min.css │ │ │ └── tabulator_bootstrap4.min.css.map │ │ │ ├── bulma │ │ │ ├── tabulator_bulma.css │ │ │ ├── tabulator_bulma.min.css │ │ │ └── tabulator_bulma.min.css.map │ │ │ ├── materialize │ │ │ ├── tabulator_materialize.css │ │ │ ├── tabulator_materialize.min.css │ │ │ └── tabulator_materialize.min.css.map │ │ │ ├── semantic-ui │ │ │ ├── tabulator_semantic-ui.css │ │ │ ├── tabulator_semantic-ui.min.css │ │ │ └── tabulator_semantic-ui.min.css.map │ │ │ ├── tabulator.css │ │ │ ├── tabulator.min.css │ │ │ ├── tabulator.min.css.map │ │ │ ├── tabulator_midnight.css │ │ │ ├── tabulator_midnight.min.css │ │ │ ├── tabulator_midnight.min.css.map │ │ │ ├── tabulator_modern.css │ │ │ ├── tabulator_modern.min.css │ │ │ ├── tabulator_modern.min.css.map │ │ │ ├── tabulator_simple.css │ │ │ ├── tabulator_simple.min.css │ │ │ ├── tabulator_simple.min.css.map │ │ │ ├── tabulator_site.css │ │ │ ├── tabulator_site.min.css │ │ │ └── tabulator_site.min.css.map │ │ └── js │ │ ├── astiloader │ │ ├── astiloader.css │ │ ├── astiloader.js │ │ └── loader.png │ │ ├── astimodaler │ │ ├── astimodaler.css │ │ ├── astimodaler.js │ │ └── cross.png │ │ ├── astinotifier │ │ ├── astinotifier.css │ │ ├── astinotifier.js │ │ └── cross.png │ │ ├── astiprogresser │ │ ├── astiprogresser.css │ │ ├── astiprogresser.js │ │ └── check.png │ │ ├── astitools │ │ └── astitools.js │ │ ├── astiws │ │ └── astiws.js │ │ ├── jquery │ │ └── jquery-3.5.1.min.js │ │ ├── jsrender │ │ └── jsrender.min.js │ │ ├── moment │ │ └── moment.min.js │ │ ├── notify │ │ └── notify.min.js │ │ └── tabulator │ │ ├── jquery_wrapper.js │ │ ├── jquery_wrapper.min.js │ │ ├── modules │ │ ├── accessor.js │ │ ├── accessor.min.js │ │ ├── ajax.js │ │ ├── ajax.min.js │ │ ├── calculation_colums.js │ │ ├── calculation_colums.min.js │ │ ├── clipboard.js │ │ ├── clipboard.min.js │ │ ├── data_tree.js │ │ ├── data_tree.min.js │ │ ├── download.js │ │ ├── download.min.js │ │ ├── edit.js │ │ ├── edit.min.js │ │ ├── export.js │ │ ├── export.min.js │ │ ├── filter.js │ │ ├── filter.min.js │ │ ├── format.js │ │ ├── format.min.js │ │ ├── frozen_columns.js │ │ ├── frozen_columns.min.js │ │ ├── frozen_rows.js │ │ ├── frozen_rows.min.js │ │ ├── group_rows.js │ │ ├── group_rows.min.js │ │ ├── history.js │ │ ├── history.min.js │ │ ├── html_table_import.js │ │ ├── html_table_import.min.js │ │ ├── keybindings.js │ │ ├── keybindings.min.js │ │ ├── menu.js │ │ ├── menu.min.js │ │ ├── moveable_columns.js │ │ ├── moveable_columns.min.js │ │ ├── moveable_rows.js │ │ ├── moveable_rows.min.js │ │ ├── mutator.js │ │ ├── mutator.min.js │ │ ├── page.js │ │ ├── page.min.js │ │ ├── persistence.js │ │ ├── persistence.min.js │ │ ├── print.js │ │ ├── print.min.js │ │ ├── reactive_data.js │ │ ├── reactive_data.min.js │ │ ├── resize_columns.js │ │ ├── resize_columns.min.js │ │ ├── resize_rows.js │ │ ├── resize_rows.min.js │ │ ├── resize_table.js │ │ ├── resize_table.min.js │ │ ├── responsive_layout.js │ │ ├── responsive_layout.min.js │ │ ├── select_row.js │ │ ├── select_row.min.js │ │ ├── sort.js │ │ ├── sort.min.js │ │ ├── validate.js │ │ └── validate.min.js │ │ ├── tabulator.js │ │ ├── tabulator.min.js │ │ ├── tabulator_core.js │ │ └── tabulator_core.min.js ├── icon.icns ├── icon.ico └── icon.png ├── settings ├── keys.go └── settings.go ├── slm.json ├── switchfs ├── _crypto │ ├── ecb.go │ └── xts.go ├── cnmt.go ├── fs.go ├── nacp.go ├── nca.go ├── ncaHeader.go ├── nsp.go ├── pfs0.go ├── romfs.go ├── splitFileReader.go └── xci.go └── updates_ui.png /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | resources/app/vendor 3 | output -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Switch library manager 2 | Easily manage your switch game backups 3 | 4 | ![Image description](https://raw.githubusercontent.com/giwty/nsp-update/master/updates_ui.png) 5 | 6 | ![Image description](https://raw.githubusercontent.com/giwty/nsp-update/master/dlc_ui.png) 7 | 8 | ![Image description](https://raw.githubusercontent.com/giwty/nsp-update/master/cmd.png) 9 | 10 | #### Features: 11 | - Cross platform, works on Windows / Mac / Linux 12 | - GUI and command line interfaces 13 | - Scan your local switch backup library (NSP/NSZ/XCI) 14 | - Read titleId/version by decrypting NSP/XCI/NSZ (requires prod.keys) 15 | - If no prod.keys present, fallback to read titleId/version by parsing file name (example: `Super Mario Odyssey [0100000000010000][v0].nsp`). 16 | - Lists missing update files (for games and DLC) 17 | - Lists missing DLCs 18 | - Automatically organize games per folder 19 | - Rename files based on metadata read from NSP 20 | - Delete old update files (in case you have multiple update files for the same game, only the latest will remain) 21 | - Delete empty folders 22 | - Zero dependencies, all crypto operations implemented in Go. 23 | 24 | ## Keys (optional) 25 | Having a prod.keys file will allow you to ensure the files you have a correctly classified. 26 | The app will look for the "prod.keys" file in the app folder or under ${HOME}/.switch/ 27 | You can also specify a custom location in the settings.json (see below) 28 | 29 | Note: Only the header_key, and the key_area_key_application_XX keys are required. 30 | 31 | ## Settings 32 | During the App first launch a "settings.json" file will be created, that allows for granular control over the Apps execution. 33 | 34 | You can customize the folder/file re-naming, as well as turn on/off features. 35 | 36 | ``` 37 | { 38 | "versions_etag": "W/\"c3f5ecb3392d61:0\"", 39 | "titles_etag": "W/\"4a4fcc163a92d61:0\"", 40 | "prod_keys": "", 41 | "folder": "", 42 | "scan_folders": [], 43 | "gui": false, 44 | "debug": false, # Deprecated, no longer works 45 | "check_for_missing_updates": true, 46 | "check_for_missing_dlc": true, 47 | "organize_options": { 48 | "create_folder_per_game": false, 49 | "rename_files": false, 50 | "delete_empty_folders": false, 51 | "delete_old_update_files": false, 52 | "folder_name_template": "{TITLE_NAME}", 53 | "switch_safe_file_names": true, 54 | "file_name_template": "{TITLE_NAME} ({DLC_NAME})[{TITLE_ID}][v{VERSION}]" 55 | }, 56 | "scan_recursively": true, 57 | "gui_page_size": 100 58 | } 59 | ``` 60 | 61 | ## Naming template 62 | The following template elements are supported: 63 | - {TITLE_NAME} - game name 64 | - {TITLE_ID} - title id 65 | - {VERSION} - version id (only applicable to files) 66 | - {VERSION_TXT} - version number (like 1.0.0) (only applicable to files) 67 | - {REGION} - region 68 | - {TYPE} - impacts DLCs/updates, will appear as ["UPD","DLC"] 69 | - {DLC_NAME} - DLC name (only applicable to DLCs) 70 | 71 | ## Reporting issues 72 | Please set debug mode to 'true', and attach the slm.log to allow for quicker resolution. 73 | 74 | ## Usage 75 | ##### Windows 76 | - Extract the zip file 77 | - Double click the Exe file 78 | - If you want to use command line mode, update the settings.json with `'GUI':false` 79 | - Open `cmd` 80 | - Run `switch-library-manager.exe` 81 | - Optionally -f `X:\folder\containing\nsp\files"` 82 | - Optionally add `-r` to recursively scan for nested folders 83 | - Edit the settings.json file for additional options 84 | 85 | 86 | ##### macOS or Linux 87 | - Extract the zip file 88 | - Double click the App file 89 | - If you want to use command line mode, update the settings.json with `'GUI':false` 90 | - Open your Terminal 91 | - `cd` to the folder containing `switch-library-manager` 92 | - `chmod +x switch-library-manager` to make it executable 93 | - Run `./switch-library-manager' 94 | - Optionally -f `X:\folder\containing\nsp\files"` 95 | - Optionally add `-r` to recursively scan for nested folders 96 | - Edit the settings.json file for additional options 97 | 98 | ## Building 99 | - Install and setup Go 100 | - Clone the repo: `git clone https://github.com/giwty/switch-library-manager.git` 101 | - Get the bundler `go get -u github.com/asticode/go-astilectron-bundler/...` 102 | - Install bundler `go install github.com/asticode/go-astilectron-bundler/astilectron-bundler` 103 | - Copy bundler binary to the source folder `cd switch-library-manager` and then `mv $HOME/go/bin/astilectron-bundler .` 104 | - Execute `./astilectron-bundler` 105 | - Binaries will be available under output 106 | 107 | #### Thanks 108 | This program relies on [blawar's titledb](https://github.com/blawar/titledb), to get the latest titles and versions. 109 | -------------------------------------------------------------------------------- /bundler.json: -------------------------------------------------------------------------------- 1 | { 2 | "app_name": "Switch-Library-Manager", 3 | "icon_path_darwin": "resources/icon.icns", 4 | "icon_path_linux": "resources/icon.png", 5 | "icon_path_windows": "resources/icon.ico", 6 | "environments": [ 7 | { 8 | "arch": "amd64", 9 | "os": "darwin", 10 | "env": { 11 | "CGO_ENABLED": "1", 12 | "CGO_CFLAGS": "-mmacosx-version-min=10.11", 13 | "CGO_LDFLAGS": "-mmacosx-version-min=10.11" 14 | } 15 | }, 16 | {"arch": "amd64", "os": "linux"}, 17 | {"arch": "amd64", "os": "windows"} 18 | ] 19 | } -------------------------------------------------------------------------------- /cmd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giwty/switch-library-manager/f69daa7e5e3bc9fc2cc17ad839a12ac3bf5e0372/cmd.png -------------------------------------------------------------------------------- /db/persistentDB.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "fmt" 7 | "github.com/boltdb/bolt" 8 | "github.com/giwty/switch-library-manager/settings" 9 | "go.uber.org/zap" 10 | "log" 11 | "path/filepath" 12 | ) 13 | 14 | const ( 15 | DB_INTERNAL_TABLENAME = "internal-metadata" 16 | ) 17 | 18 | type PersistentDB struct { 19 | db *bolt.DB 20 | } 21 | 22 | func NewPersistentDB(baseFolder string) (*PersistentDB, error) { 23 | // Open the my.db data file in your current directory. 24 | // It will be created if it doesn't exist. 25 | db, err := bolt.Open(filepath.Join(baseFolder, "slm.db"), 0600, &bolt.Options{Timeout: 1 * 60}) 26 | if err != nil { 27 | log.Fatal(err) 28 | return nil, err 29 | } 30 | 31 | //set DB version 32 | err = db.View(func(tx *bolt.Tx) error { 33 | b := tx.Bucket([]byte(DB_INTERNAL_TABLENAME)) 34 | if b == nil { 35 | b, err := tx.CreateBucket([]byte(DB_INTERNAL_TABLENAME)) 36 | if b == nil || err != nil { 37 | return fmt.Errorf("create bucket: %s", err) 38 | } 39 | err = b.Put([]byte("app_version"), []byte(settings.SLM_VERSION)) 40 | if err != nil { 41 | zap.S().Warnf("failed to save app_version - %v", err) 42 | return err 43 | } 44 | } 45 | return nil 46 | }) 47 | 48 | return &PersistentDB{db: db}, nil 49 | } 50 | 51 | func (pd *PersistentDB) Close() { 52 | pd.db.Close() 53 | } 54 | 55 | func (pd *PersistentDB) ClearTable(tableName string) error { 56 | err := pd.db.Update(func(tx *bolt.Tx) error { 57 | err := tx.DeleteBucket([]byte(tableName)) 58 | return err 59 | }) 60 | return err 61 | } 62 | 63 | func (pd *PersistentDB) AddEntry(tableName string, key string, value interface{}) error { 64 | var err error 65 | err = pd.db.Update(func(tx *bolt.Tx) error { 66 | b := tx.Bucket([]byte(tableName)) 67 | if b == nil { 68 | b, err = tx.CreateBucket([]byte(tableName)) 69 | if b == nil || err != nil { 70 | return fmt.Errorf("create bucket: %s", err) 71 | } 72 | } 73 | var bytesBuff bytes.Buffer 74 | encoder := gob.NewEncoder(&bytesBuff) 75 | err := encoder.Encode(value) 76 | if err != nil { 77 | return err 78 | } 79 | err = b.Put([]byte(key), bytesBuff.Bytes()) 80 | return err 81 | }) 82 | return err 83 | } 84 | 85 | func (pd *PersistentDB) GetEntry(tableName string, key string, value interface{}) error { 86 | err := pd.db.View(func(tx *bolt.Tx) error { 87 | 88 | b := tx.Bucket([]byte(tableName)) 89 | if b == nil { 90 | return nil 91 | } 92 | v := b.Get([]byte(key)) 93 | if v == nil { 94 | return nil 95 | } 96 | d := gob.NewDecoder(bytes.NewReader(v)) 97 | 98 | // Decoding the serialized data 99 | err := d.Decode(value) 100 | if err != nil { 101 | return err 102 | } 103 | return nil 104 | }) 105 | return err 106 | } 107 | 108 | /*func (pd *PersistentDB) GetEntries() (map[string]*switchfs.ContentMetaAttributes, error) { 109 | pd.db.View(func(tx *bolt.Tx) error { 110 | // Assume bucket exists and has keys 111 | b := tx.Bucket([]byte(METADATA_TABLENAME)) 112 | 113 | c := b.Cursor() 114 | 115 | for k, v := c.First(); k != nil; k, v = c.Next() { 116 | fmt.Printf("key=%s, value=%s\n", k, v) 117 | } 118 | 119 | return nil 120 | }) 121 | }*/ 122 | -------------------------------------------------------------------------------- /db/switchTitlesDB.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "strings" 7 | ) 8 | 9 | type TitleAttributes struct { 10 | Id string `json:"id"` 11 | Name string `json:"name,omitempty"` 12 | Version json.Number `json:"version,omitempty"` 13 | Region string `json:"region,omitempty"` 14 | ReleaseDate int `json:"releaseDate,omitempty"` 15 | Publisher string `json:"publisher,omitempty"` 16 | IconUrl string `json:"iconUrl,omitempty"` 17 | Screenshots []string `json:"screenshots,omitempty"` 18 | BannerUrl string `json:"bannerUrl,omitempty"` 19 | Description string `json:"description,omitempty"` 20 | Size int `json:"size,omitempty"` 21 | } 22 | 23 | type SwitchTitle struct { 24 | Attributes TitleAttributes 25 | Updates map[int]string 26 | Dlc map[string]TitleAttributes 27 | } 28 | 29 | type SwitchTitlesDB struct { 30 | TitlesMap map[string]*SwitchTitle 31 | } 32 | 33 | func CreateSwitchTitleDB(titlesFile, versionsFile io.Reader) (*SwitchTitlesDB, error) { 34 | //parse the titles objects 35 | var titles = map[string]TitleAttributes{} 36 | err := decodeToJsonObject(titlesFile, &titles) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | //parse the titles objects 42 | //titleID -> versionId-> release date 43 | var versions = map[string]map[int]string{} 44 | err = decodeToJsonObject(versionsFile, &versions) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | result := SwitchTitlesDB{TitlesMap: map[string]*SwitchTitle{}} 50 | for id, attr := range titles { 51 | id = strings.ToLower(id) 52 | 53 | //TitleAttributes id rules: 54 | //main TitleAttributes ends with 000 55 | //Updates ends with 800 56 | //Dlc have a running counter (starting with 001) in the 4 last chars 57 | idPrefix := id[0 : len(id)-4] 58 | switchTitle := &SwitchTitle{Dlc: map[string]TitleAttributes{}} 59 | if t, ok := result.TitlesMap[idPrefix]; ok { 60 | switchTitle = t 61 | } 62 | result.TitlesMap[idPrefix] = switchTitle 63 | 64 | //process Updates 65 | if strings.HasSuffix(id, "800") { 66 | updates := versions[id[0:len(id)-3]+"000"] 67 | switchTitle.Updates = updates 68 | continue 69 | } 70 | 71 | //process main TitleAttributes 72 | if strings.HasSuffix(id, "000") { 73 | switchTitle.Attributes = attr 74 | continue 75 | } 76 | 77 | //not an update, and not main TitleAttributes, so treat it as a DLC 78 | switchTitle.Dlc[id] = attr 79 | 80 | } 81 | 82 | return &result, nil 83 | } 84 | -------------------------------------------------------------------------------- /db/utils.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | bytes2 "bytes" 5 | "encoding/json" 6 | "errors" 7 | "go.uber.org/zap" 8 | "io" 9 | "io/ioutil" 10 | "net" 11 | "net/http" 12 | "os" 13 | "time" 14 | ) 15 | 16 | type ProgressUpdater interface { 17 | UpdateProgress(curr int, total int, message string) 18 | } 19 | 20 | func LoadAndUpdateFile(url string, filePath string, etag string) (*os.File, string, error) { 21 | 22 | //create file if not exist 23 | if _, err := os.Stat(filePath); os.IsNotExist(err) { 24 | _, err = os.Create(filePath) 25 | if err != nil { 26 | zap.S().Errorf("Failed to create file %v - %v\n", filePath, err) 27 | return nil, "", err 28 | } 29 | } 30 | 31 | var file *os.File = nil 32 | 33 | //try to check if there is a new version 34 | //if so, save the file 35 | bytes, newEtag, err := downloadBytesFromUrl(url, etag) 36 | if err == nil { 37 | //validate json structure 38 | var test map[string]interface{} 39 | err = decodeToJsonObject(bytes2.NewReader(bytes), &test) 40 | if err == nil { 41 | file, err = saveFile(bytes, filePath) 42 | etag = newEtag 43 | } else { 44 | zap.S().Infof("ignoring new update [%v], reason - [mailformed json file]", url) 45 | } 46 | } else { 47 | zap.S().Infof("file [%v] was not downloaded, reason - [%v]", url, err) 48 | } 49 | 50 | if file == nil { 51 | //load file 52 | file, err = os.Open(filePath) 53 | if err != nil { 54 | zap.S().Infof("ignoring new update [%v], reason - [mailformed json file]", url) 55 | return nil, "", err 56 | } 57 | 58 | fileInfo, err := os.Stat(filePath) 59 | if err != nil || fileInfo.Size() == 0 { 60 | zap.S().Infof("Local file is empty, or corrupted") 61 | return nil, "", errors.New("unable to download switch titles db") 62 | } 63 | } 64 | 65 | return file, etag, err 66 | } 67 | 68 | func decodeToJsonObject(reader io.Reader, target interface{}) error { 69 | err := json.NewDecoder(reader).Decode(target) 70 | return err 71 | } 72 | 73 | func downloadBytesFromUrl(url string, etag string) ([]byte, string, error) { 74 | req, err := http.NewRequest("GET", url, nil) 75 | if err != nil { 76 | return nil, "", err 77 | } 78 | req.Header.Set("If-None-Match", etag) 79 | transport := &http.Transport{ 80 | DialContext: (&net.Dialer{ 81 | Timeout: 3 * time.Second, 82 | }).DialContext, 83 | } 84 | client := http.Client{ 85 | Transport: transport, 86 | } 87 | resp, err := client.Do(req) 88 | if err != nil { 89 | return nil, "", err 90 | } 91 | 92 | if resp.StatusCode >= 400 { 93 | return nil, "", errors.New("got a non 200 response - " + resp.Status) 94 | } 95 | defer resp.Body.Close() 96 | //getting the new etag 97 | etag = resp.Header.Get("Etag") 98 | 99 | if resp.StatusCode == http.StatusOK { 100 | body, err := ioutil.ReadAll(resp.Body) 101 | if err != nil { 102 | return nil, "", err 103 | } 104 | return body, etag, nil 105 | } 106 | 107 | return nil, "", errors.New("no new updates") 108 | } 109 | 110 | func saveFile(bytes []byte, fileName string) (*os.File, error) { 111 | 112 | err := ioutil.WriteFile(fileName, bytes, 0644) 113 | if err != nil { 114 | return nil, err 115 | } 116 | 117 | file, err := os.Open(fileName) 118 | if err != nil { 119 | return nil, err 120 | } 121 | return file, nil 122 | } 123 | -------------------------------------------------------------------------------- /dlc_ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giwty/switch-library-manager/f69daa7e5e3bc9fc2cc17ad839a12ac3bf5e0372/dlc_ui.png -------------------------------------------------------------------------------- /fileio/splitFileUtil.go: -------------------------------------------------------------------------------- 1 | package fileio 2 | 3 | import ( 4 | "errors" 5 | "github.com/giwty/switch-library-manager/switchfs" 6 | "os" 7 | ) 8 | 9 | func ReadSplitFileMetadata(filePath string) (map[string]*switchfs.ContentMetaAttributes, error) { 10 | //check if this is a NS* or XC* file 11 | _, err := switchfs.ReadPfs0File(filePath) 12 | isXCI := false 13 | if err != nil { 14 | _, err = readXciHeader(filePath) 15 | if err != nil { 16 | return nil, errors.New("split file is not an XCI/XCZ or NSP/NSZ") 17 | } 18 | isXCI = true 19 | } 20 | 21 | if isXCI { 22 | return switchfs.ReadXciMetadata(filePath) 23 | } else { 24 | return switchfs.ReadNspMetadata(filePath) 25 | } 26 | } 27 | 28 | func readXciHeader(filePath string) ([]byte, error) { 29 | file, err := os.Open(filePath) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | defer file.Close() 35 | 36 | header := make([]byte, 0x200) 37 | _, err = file.Read(header) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | if string(header[0x100:0x104]) != "HEAD" { 43 | return nil, errors.New("not an XCI/XCZ file") 44 | } 45 | return header, nil 46 | } 47 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/giwty/switch-library-manager 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/asticode/go-astikit v0.8.0 7 | github.com/asticode/go-astilectron v0.16.0 8 | github.com/asticode/go-astilectron-bootstrap v0.4.1 9 | github.com/avast/retry-go v2.6.1+incompatible 10 | github.com/boltdb/bolt v1.3.1 11 | github.com/go-openapi/strfmt v0.19.2 // indirect 12 | github.com/jedib0t/go-pretty v4.3.0+incompatible 13 | github.com/magiconair/properties v1.8.1 14 | github.com/mcuadros/go-version v0.0.0-20190830083331-035f6764e8d2 15 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect 16 | github.com/schollz/progressbar/v3 v3.5.0 17 | github.com/stretchr/testify v1.5.1 // indirect 18 | go.uber.org/zap v1.16.0 19 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect 20 | gopkg.in/yaml.v2 v2.2.8 // indirect 21 | robpike.io/nihongo v0.0.0-20200511095354-a985f0929cfa 22 | ) 23 | -------------------------------------------------------------------------------- /library_ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giwty/switch-library-manager/f69daa7e5e3bc9fc2cc17ad839a12ac3bf5e0372/library_ui.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/giwty/switch-library-manager/settings" 6 | "go.uber.org/zap" 7 | "net/url" 8 | "os" 9 | "path/filepath" 10 | "runtime" 11 | "strings" 12 | ) 13 | 14 | func main() { 15 | 16 | exePath, err := os.Executable() 17 | if err != nil { 18 | fmt.Println("failed to get executable directory, please ensure app has sufficient permissions. aborting") 19 | return 20 | } 21 | 22 | workingFolder := filepath.Dir(exePath) 23 | 24 | if runtime.GOOS == "darwin" { 25 | if strings.Contains(workingFolder, ".app") { 26 | appIndex := strings.Index(workingFolder, ".app") 27 | sepIndex := strings.LastIndex(workingFolder[:appIndex], string(os.PathSeparator)) 28 | workingFolder = workingFolder[:sepIndex] 29 | } 30 | } 31 | 32 | appSettings := settings.ReadSettings(workingFolder) 33 | 34 | logger := createLogger(workingFolder, appSettings.Debug) 35 | 36 | defer logger.Sync() // flushes buffer, if any 37 | sugar := logger.Sugar() 38 | 39 | sugar.Info("[SLM starts]") 40 | sugar.Infof("[Executable: %v]", exePath) 41 | sugar.Infof("[Working directory: %v]", workingFolder) 42 | 43 | appSettings.GUI = true 44 | 45 | files, err := AssetDir(workingFolder) 46 | if files == nil && err == nil { 47 | appSettings.GUI = false 48 | } 49 | 50 | if appSettings.GUI { 51 | CreateGUI(workingFolder, sugar).Start() 52 | } else { 53 | CreateConsole(workingFolder, sugar).Start() 54 | } 55 | 56 | } 57 | 58 | func createLogger(workingFolder string, debug bool) *zap.Logger { 59 | var config zap.Config 60 | if debug { 61 | config = zap.NewDevelopmentConfig() 62 | } else { 63 | config = zap.NewDevelopmentConfig() 64 | config.Level = zap.NewAtomicLevelAt(zap.InfoLevel) 65 | } 66 | logPath := filepath.Join(workingFolder, "slm.log") 67 | // delete old file 68 | os.Remove(logPath) 69 | 70 | if runtime.GOOS == "windows" { 71 | zap.RegisterSink("winfile", func(u *url.URL) (zap.Sink, error) { 72 | // Remove leading slash left by url.Parse() 73 | return os.OpenFile(u.Path[1:], os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644) 74 | }) 75 | logPath = "winfile:///" + logPath 76 | } 77 | 78 | config.OutputPaths = []string{logPath} 79 | config.ErrorOutputPaths = []string{logPath} 80 | logger, err := config.Build() 81 | if err != nil { 82 | fmt.Printf("failed to create logger - %v", err) 83 | panic(1) 84 | } 85 | zap.ReplaceGlobals(logger) 86 | return logger 87 | } 88 | -------------------------------------------------------------------------------- /process/incompleteTitleProcessor.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "fmt" 5 | "github.com/giwty/switch-library-manager/db" 6 | "github.com/giwty/switch-library-manager/switchfs" 7 | "go.uber.org/zap" 8 | "sort" 9 | "strconv" 10 | ) 11 | 12 | type IncompleteTitle struct { 13 | Attributes db.TitleAttributes 14 | Meta *switchfs.ContentMetaAttributes 15 | LocalUpdate int `json:"local_update"` 16 | LatestUpdate int `json:"latest_update"` 17 | LatestUpdateDate string `json:"latest_update_date"` 18 | MissingDLC []string `json:"missing_dlc"` 19 | } 20 | 21 | func ScanForMissingUpdates(localDB map[string]*db.SwitchGameFiles, 22 | switchDB map[string]*db.SwitchTitle) map[string]IncompleteTitle { 23 | 24 | result := map[string]IncompleteTitle{} 25 | 26 | //iterate over local files, and compare to remote versions 27 | for idPrefix, switchFile := range localDB { 28 | 29 | if switchFile.BaseExist == false { 30 | zap.S().Infof("missing base for game %v", idPrefix) 31 | continue 32 | } 33 | 34 | if _, ok := switchDB[idPrefix]; !ok { 35 | continue 36 | } 37 | 38 | switchTitle := IncompleteTitle{Attributes: switchDB[idPrefix].Attributes, Meta: switchFile.File.Metadata} 39 | //sort the available local versions 40 | localVersions := make([]int, len(switchFile.Updates)) 41 | i := 0 42 | for k := range switchFile.Updates { 43 | localVersions[i] = k 44 | i++ 45 | } 46 | sort.Ints(localVersions) 47 | 48 | //sort the available remote versions 49 | remoteVersions := make([]int, len(switchDB[idPrefix].Updates)) 50 | i = 0 51 | for k := range switchDB[idPrefix].Updates { 52 | remoteVersions[i] = k 53 | i++ 54 | } 55 | sort.Ints(remoteVersions) 56 | switchTitle.LocalUpdate = 0 57 | switchTitle.LatestUpdate = 0 58 | if len(localVersions) != 0 { 59 | switchTitle.LocalUpdate = localVersions[len(localVersions)-1] 60 | } 61 | 62 | //process updates 63 | if len(remoteVersions) != 0 { 64 | switchTitle.LatestUpdate = remoteVersions[len(remoteVersions)-1] 65 | switchTitle.LatestUpdateDate = switchDB[idPrefix].Updates[remoteVersions[len(remoteVersions)-1]] 66 | if switchTitle.LocalUpdate < switchTitle.LatestUpdate { 67 | result[switchDB[idPrefix].Attributes.Id] = switchTitle 68 | } 69 | } 70 | 71 | if len(switchDB[idPrefix].Dlc) == 0 { 72 | continue 73 | } 74 | 75 | //process dlc 76 | for k, availableDlc := range switchDB[idPrefix].Dlc { 77 | 78 | if localDlc, ok := switchFile.Dlc[k]; ok { 79 | latestDlcVersion, err := availableDlc.Version.Int64() 80 | if err != nil { 81 | continue 82 | } 83 | 84 | if localDlc.Metadata == nil { 85 | continue 86 | } 87 | if localDlc.Metadata.Version < int(latestDlcVersion) { 88 | updateDate := "-" 89 | if availableDlc.ReleaseDate != 0 { 90 | updateDate = strconv.Itoa(availableDlc.ReleaseDate) 91 | if len(updateDate) > 7 { 92 | updateDate = updateDate[0:4] + "-" + updateDate[4:6] + "-" + updateDate[6:] 93 | } 94 | } 95 | 96 | result[availableDlc.Id] = IncompleteTitle{ 97 | Attributes: availableDlc, 98 | LatestUpdate: int(latestDlcVersion), 99 | LocalUpdate: localDlc.Metadata.Version, 100 | LatestUpdateDate: updateDate, 101 | Meta: localDlc.Metadata} 102 | } 103 | } 104 | } 105 | 106 | } 107 | return result 108 | } 109 | 110 | func ScanForMissingDLC(localDB map[string]*db.SwitchGameFiles, 111 | switchDB map[string]*db.SwitchTitle, ignoreTitleIds map[string]struct{}) map[string]IncompleteTitle { 112 | result := map[string]IncompleteTitle{} 113 | 114 | //iterate over local files, and compare to remote versions 115 | for idPrefix, switchFile := range localDB { 116 | 117 | if switchFile.BaseExist == false { 118 | continue 119 | } 120 | 121 | if _, ok := switchDB[idPrefix]; !ok { 122 | continue 123 | } 124 | switchTitle := IncompleteTitle{Attributes: switchDB[idPrefix].Attributes} 125 | 126 | //process dlc 127 | if len(switchDB[idPrefix].Dlc) != 0 { 128 | for k, v := range switchDB[idPrefix].Dlc { 129 | if _, ok := ignoreTitleIds[k]; ok { 130 | continue 131 | } 132 | 133 | if _, ok := switchFile.Dlc[k]; !ok { 134 | switchTitle.MissingDLC = append(switchTitle.MissingDLC, fmt.Sprintf("%v [%v]", v.Name, v.Id)) 135 | } 136 | } 137 | if len(switchTitle.MissingDLC) != 0 { 138 | result[switchDB[idPrefix].Attributes.Id] = switchTitle 139 | } 140 | } 141 | } 142 | return result 143 | } 144 | 145 | func ScanForBrokenFiles(localDB map[string]*db.SwitchGameFiles) []db.SwitchFileInfo { 146 | var result []db.SwitchFileInfo 147 | 148 | //iterate over local files, and compare to remote versions 149 | for _, switchFile := range localDB { 150 | 151 | if switchFile.BaseExist == false { 152 | for _, f := range switchFile.Dlc { 153 | result = append(result, f) 154 | } 155 | for _, f := range switchFile.Updates { 156 | result = append(result, f) 157 | } 158 | } 159 | } 160 | return result 161 | } 162 | -------------------------------------------------------------------------------- /process/organizefolderStructure_test.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "robpike.io/nihongo" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | //var folderIllegalCharsRegex = regexp.MustCompile(`[./\\?%*:;=|"<>]`) 10 | 11 | func TestRename(t *testing.T) { 12 | name := "Pokémon™: Let’s Go, Eevee! 포탈 나이츠" 13 | name = folderIllegalCharsRegex.ReplaceAllString(name, "") 14 | safe := cjk.FindAllString(name, -1) 15 | name = strings.Join(safe, "") 16 | name = nihongo.RomajiString(name) 17 | } 18 | -------------------------------------------------------------------------------- /prod.keys: -------------------------------------------------------------------------------- 1 | key_area_key_application_00 = 2 | key_area_key_application_01 = 3 | key_area_key_application_02 = 4 | key_area_key_application_03 = 5 | key_area_key_application_04 = 6 | key_area_key_application_05 = 7 | key_area_key_application_06 = 8 | key_area_key_application_07 = 9 | key_area_key_application_08 = 10 | key_area_key_application_09 = 11 | key_area_key_application_0a = 12 | header_key = -------------------------------------------------------------------------------- /resources/app/lib/js/astiloader/astiloader.css: -------------------------------------------------------------------------------- 1 | .astiloader { 2 | color: #fff; 3 | display: none; 4 | height: 100%; 5 | left: 0; 6 | position: fixed; 7 | top: 0; 8 | width: 100%; 9 | z-index: 1; 10 | } 11 | .astiloader-background { 12 | background-color: #000; 13 | height: 100%; 14 | left: 0; 15 | opacity: 0.7; 16 | position: absolute; 17 | top: 0; 18 | width: 100%; 19 | } 20 | .astiloader-table { 21 | display: table; 22 | height: 100%; 23 | left: 0; 24 | position: absolute; 25 | top: 0; 26 | width: 100%; 27 | } 28 | .astiloader-content { 29 | display: table-cell; 30 | height: 100%; 31 | text-align: center; 32 | vertical-align: middle; 33 | width: 100%; 34 | } 35 | @keyframes asticode-spin { 36 | 0% { 37 | -webkit-transform:rotate(0deg); 38 | transform:rotate(0deg) 39 | } 40 | to { 41 | -webkit-transform:rotate(1turn); 42 | transform:rotate(1turn) 43 | } 44 | } 45 | .astiloader-content img { 46 | animation: asticode-spin 2s linear infinite; 47 | width: 5em; 48 | } -------------------------------------------------------------------------------- /resources/app/lib/js/astiloader/astiloader.js: -------------------------------------------------------------------------------- 1 | if (typeof asticode === "undefined") { 2 | var asticode = {}; 3 | } 4 | asticode.loader = { 5 | scriptDir: document.currentScript.src.match(/.*\//), 6 | hide: function() { 7 | document.getElementById("astiloader").style.display = "none"; 8 | }, 9 | init: function() { 10 | document.body.innerHTML = ` 11 |
12 |
13 |
14 |
15 | ` + document.body.innerHTML 16 | }, 17 | show: function() { 18 | document.getElementById("astiloader").style.display = "block"; 19 | } 20 | }; -------------------------------------------------------------------------------- /resources/app/lib/js/astiloader/loader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giwty/switch-library-manager/f69daa7e5e3bc9fc2cc17ad839a12ac3bf5e0372/resources/app/lib/js/astiloader/loader.png -------------------------------------------------------------------------------- /resources/app/lib/js/astimodaler/astimodaler.css: -------------------------------------------------------------------------------- 1 | .astimodaler { 2 | color: #fff; 3 | display: none; 4 | height: 100%; 5 | left: 0; 6 | position: fixed; 7 | top: 0; 8 | width: 100%; 9 | z-index: 1; 10 | } 11 | .astimodaler-background { 12 | background-color: #000; 13 | height: 100%; 14 | left: 0; 15 | opacity: 0.7; 16 | position: absolute; 17 | top: 0; 18 | width: 100%; 19 | } 20 | .astimodaler-table { 21 | display: table; 22 | height: 100%; 23 | left: 0; 24 | position: absolute; 25 | top: 0; 26 | width: 100%; 27 | } 28 | .astimodaler-wrapper { 29 | display: table-cell; 30 | height: 100%; 31 | padding: 2em; 32 | text-align: center; 33 | vertical-align: middle; 34 | width: 100%; 35 | } 36 | #astimodaler-body { 37 | background-color: #f1f1f1; 38 | border-radius: 5px; 39 | color: #333; 40 | margin: auto; 41 | padding: 2em; 42 | position: relative; 43 | } 44 | .astimodaler-close { 45 | cursor: pointer; 46 | padding: 0.65em; 47 | position: absolute; 48 | right: 0; 49 | top: 0; 50 | width: 2.4em; 51 | } 52 | #astimodaler-content { 53 | text-align: left; 54 | } 55 | .astimodaler-title { 56 | border-bottom: solid 1px #aaa; 57 | font-size: 1.2em; 58 | margin-bottom: 1em; 59 | } 60 | .astimodaler-error { 61 | background-color: #f2dede; 62 | border: solid 1px #dca7a7; 63 | border-radius: 0.5em; 64 | color: #a94442; 65 | display: none; 66 | margin-bottom: 1em; 67 | padding: 1em; 68 | text-align: center; 69 | } 70 | .astimodaler-label { 71 | margin-bottom: 0.3em; 72 | } 73 | .astimodaler-required { 74 | color: red; 75 | } 76 | .astimodaler-field-select { 77 | border: solid 1px #ccc; 78 | font: inherit; 79 | margin-bottom: 0.7em; 80 | width: 100%; 81 | } 82 | .astimodaler-field-text, .astimodaler-field-textarea { 83 | border: solid 1px #ccc; 84 | border-radius: 0.5em; 85 | box-shadow: none; 86 | font: inherit; 87 | margin-bottom: 0.7em; 88 | padding: 0.5em 0.7em; 89 | width: 100%; 90 | } 91 | .astimodaler-field-textarea { 92 | height: 15em; 93 | } 94 | .astimodaler-field-text:focus, .astimodaler-field-select:focus { 95 | outline: none; 96 | } 97 | .astimodaler-field-text, .astimodaler-field-textarea { 98 | display: block; 99 | } 100 | .astimodaler-field-submit { 101 | border-radius: 0.5em; 102 | cursor: pointer; 103 | display: inline-block; 104 | font-size: 1em; 105 | padding: 0.7em 1em; 106 | text-align: center; 107 | vertical-align: middle; 108 | width: 100%; 109 | } 110 | #astimodaler-content > div > div:last-child { 111 | margin-bottom: 0; 112 | } -------------------------------------------------------------------------------- /resources/app/lib/js/astimodaler/cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giwty/switch-library-manager/f69daa7e5e3bc9fc2cc17ad839a12ac3bf5e0372/resources/app/lib/js/astimodaler/cross.png -------------------------------------------------------------------------------- /resources/app/lib/js/astinotifier/astinotifier.css: -------------------------------------------------------------------------------- 1 | .astinotifier { 2 | right: 0; 3 | position: fixed; 4 | top: 0; 5 | z-index: 1; 6 | width: 100%; 7 | } 8 | @media (min-width: 768px) { 9 | .astinotifier { 10 | max-width: 50%; 11 | } 12 | } 13 | .astinotifier-wrapper { 14 | padding: 0.5em 0.5em 0 0.5em; 15 | } 16 | .astinotifier-item { 17 | width: 100%; 18 | border-radius: 4px; 19 | display: table; 20 | padding: 1.2em 0; 21 | text-shadow: 0 1px 0 rgba(255, 255, 255, .2); 22 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); 23 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); 24 | box-sizing: border-box; 25 | } 26 | .astinotifier-item.success { 27 | color: #3c763d; 28 | background-color: #dff0d8; 29 | background-image: -webkit-linear-gradient(top, #dff0d8 0, #c8e5bc 100%); 30 | background-image: -o-linear-gradient(top, #dff0d8 0, #c8e5bc 100%); 31 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc)); 32 | background-image: linear-gradient(to bottom, #dff0d8 0, #c8e5bc 100%); 33 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0); 34 | background-repeat: repeat-x; 35 | border-color: #b2dba1 36 | } 37 | .astinotifier-item.info { 38 | color: #31708f; 39 | background-color: #d9edf7; 40 | background-image: -webkit-linear-gradient(top, #d9edf7 0, #b9def0 100%); 41 | background-image: -o-linear-gradient(top, #d9edf7 0, #b9def0 100%); 42 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0)); 43 | background-image: linear-gradient(to bottom, #d9edf7 0, #b9def0 100%); 44 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0); 45 | background-repeat: repeat-x; 46 | border-color: #9acfea 47 | } 48 | .astinotifier-item.warning { 49 | color: #8a6d3b; 50 | background-color: #fcf8e3; 51 | background-image: -webkit-linear-gradient(top, #fcf8e3 0, #f8efc0 100%); 52 | background-image: -o-linear-gradient(top, #fcf8e3 0, #f8efc0 100%); 53 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0)); 54 | background-image: linear-gradient(to bottom, #fcf8e3 0, #f8efc0 100%); 55 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0); 56 | background-repeat: repeat-x; 57 | border-color: #f5e79e 58 | } 59 | .astinotifier-item.error { 60 | color: #a94442; 61 | background-color: #f2dede; 62 | background-image: -webkit-linear-gradient(top, #f2dede 0, #e7c3c3 100%); 63 | background-image: -o-linear-gradient(top, #f2dede 0, #e7c3c3 100%); 64 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3)); 65 | background-image: linear-gradient(to bottom, #f2dede 0, #e7c3c3 100%); 66 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0); 67 | background-repeat: repeat-x; 68 | border-color: #dca7a7 69 | } 70 | .astinotifier-label { 71 | display: table-cell; 72 | padding-left: 1.2em; 73 | text-align: center; 74 | vertical-align: middle; 75 | } 76 | .astinotifier-close { 77 | cursor: pointer; 78 | display: table-cell; 79 | text-align: right; 80 | vertical-align: middle; 81 | } 82 | .astinotifier-close img { 83 | padding: 0 1.2em; 84 | width: 3.5em; 85 | } -------------------------------------------------------------------------------- /resources/app/lib/js/astinotifier/astinotifier.js: -------------------------------------------------------------------------------- 1 | if (typeof asticode === "undefined") { 2 | var asticode = {}; 3 | } 4 | asticode.notifier = { 5 | scriptDir: document.currentScript.src.match(/.*\//), 6 | error: function(message) { 7 | this.notify("error", message); 8 | }, 9 | info: function(message) { 10 | this.notify("info", message); 11 | }, 12 | init: function() { 13 | document.body.innerHTML = `
` + document.body.innerHTML 14 | }, 15 | notify: function(type, message) { 16 | const wrapper = document.createElement("div"); 17 | wrapper.className = "astinotifier-wrapper"; 18 | const item = document.createElement("div"); 19 | item.className = "astinotifier-item " + type; 20 | const label = document.createElement("div"); 21 | label.className = "astinotifier-label"; 22 | label.innerHTML = message; 23 | const close = document.createElement("div"); 24 | close.className = "astinotifier-close"; 25 | close.innerHTML = ``; 26 | close.onclick = function() { 27 | wrapper.remove(); 28 | }; 29 | item.appendChild(label); 30 | item.appendChild(close); 31 | wrapper.appendChild(item); 32 | document.getElementById("astinotifier").prepend(wrapper); 33 | setTimeout(function() { 34 | close.click(); 35 | }, 5000); 36 | }, 37 | success: function(message) { 38 | this.notify("success", message); 39 | }, 40 | warning: function(message) { 41 | this.notify("warning", message); 42 | } 43 | }; -------------------------------------------------------------------------------- /resources/app/lib/js/astinotifier/cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giwty/switch-library-manager/f69daa7e5e3bc9fc2cc17ad839a12ac3bf5e0372/resources/app/lib/js/astinotifier/cross.png -------------------------------------------------------------------------------- /resources/app/lib/js/astiprogresser/astiprogresser.css: -------------------------------------------------------------------------------- 1 | .astiprogresser { 2 | display: table; 3 | width: 100%; 4 | } 5 | 6 | .astiprogresser-circle-cell, .astiprogresser-bar-cell { 7 | display: table-cell; 8 | text-align: center; 9 | vertical-align: middle; 10 | } 11 | 12 | .astiprogresser-circle-cell { 13 | max-width: 3em; 14 | width: 3em; 15 | } 16 | 17 | .astiprogresser-circle-wrapper { 18 | height: 3em; 19 | position: relative; 20 | width: 100%; 21 | } 22 | 23 | .astiprogresser-circle-cell:first-child .astiprogresser-circle-bar { 24 | float: right; 25 | width: 50%; 26 | } 27 | 28 | .astiprogresser-circle-bar { 29 | height: 1em; 30 | position: relative; 31 | top: 1em; 32 | width: 100%; 33 | } 34 | 35 | .astiprogresser-circle-bar { 36 | background-color: #62c462; 37 | } 38 | 39 | .astiprogresser.error .astiprogresser-circle-bar { 40 | background-color: #ee5f5b; 41 | } 42 | 43 | .astiprogresser-circle-cell.disabled .astiprogresser-circle-bar { 44 | background-color: #d8dee3; 45 | } 46 | 47 | .astiprogresser-circle-cell:last-child .astiprogresser-circle-bar { 48 | float: left; 49 | width: 50%; 50 | } 51 | 52 | .astiprogresser-circle { 53 | border-radius: 50%; 54 | height: 3em; 55 | left: 0; 56 | position: absolute; 57 | top: 0; 58 | width: 3em; 59 | } 60 | 61 | .astiprogresser-circle-cell.disabled .astiprogresser-circle { 62 | background-color: #ffffff; 63 | border: solid 6px #d8dee3; 64 | } 65 | 66 | .astiprogresser-circle-cell.enabled .astiprogresser-circle { 67 | background-color: #ffffff; 68 | border: solid 6px #62c462; 69 | } 70 | 71 | .astiprogresser.error .astiprogresser-circle-cell.enabled .astiprogresser-circle { 72 | background-color: #ffffff; 73 | border: solid 6px #ee5f5b; 74 | } 75 | 76 | .astiprogresser-circle-cell.done .astiprogresser-circle { 77 | background-color: #62c462; 78 | border: solid 6px #62c462; 79 | } 80 | 81 | .astiprogresser.error .astiprogresser-circle-cell.done .astiprogresser-circle { 82 | background-color: #ee5f5b; 83 | border: solid 6px #ee5f5b; 84 | } 85 | 86 | .astiprogresser-circle-check { 87 | display: none; 88 | height: 1.5em; 89 | left: 0.75em; 90 | position: absolute; 91 | top: 0.75em; 92 | } 93 | 94 | .astiprogresser-circle-cell.done .astiprogresser-circle-check { 95 | display: block; 96 | } 97 | 98 | .astiprogresser-bar-wrapper { 99 | background-color: #d8dee3; 100 | height: 1em; 101 | top: 1em; 102 | width: 100%; 103 | } 104 | 105 | .astiprogresser-label { 106 | height: 1.5em; 107 | } 108 | 109 | .astiprogresser-label { 110 | color: #62c462; 111 | } 112 | 113 | .astiprogresser.error .astiprogresser-label { 114 | color: #ee5f5b; 115 | } 116 | 117 | .astiprogresser-bar-cell.disabled .astiprogresser-label { 118 | color: #c1c1c1; 119 | } 120 | 121 | .astiprogresser-bar { 122 | background-color: #62c462; 123 | height: 100%; 124 | width: 0; 125 | } 126 | 127 | .astiprogresser.error .astiprogresser-bar { 128 | background-color: #ee5f5b; 129 | } -------------------------------------------------------------------------------- /resources/app/lib/js/astiprogresser/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giwty/switch-library-manager/f69daa7e5e3bc9fc2cc17ad839a12ac3bf5e0372/resources/app/lib/js/astiprogresser/check.png -------------------------------------------------------------------------------- /resources/app/lib/js/astitools/astitools.js: -------------------------------------------------------------------------------- 1 | if (typeof asticode === "undefined") { 2 | var asticode = {} 3 | } 4 | asticode.tools = { 5 | sendHttp: function(options) { 6 | const req = new XMLHttpRequest() 7 | req.onreadystatechange = function() { 8 | if (this.readyState === XMLHttpRequest.DONE) { 9 | // Parse data 10 | let data = {responseText: this.responseText, err: null, status: this.status} 11 | if (this.responseText.length > 0 && this.getResponseHeader("content-type").indexOf("application/json") > -1) { 12 | try { 13 | data.responseJSON = JSON.parse(this.responseText) 14 | } catch (e) { 15 | data.err = e 16 | } 17 | } 18 | 19 | // Callbacks 20 | if (data.err === null && this.status >= 200 && this.status < 300) { 21 | if (typeof options.success !== "undefined") options.success(data) 22 | } else { 23 | if (typeof options.error !== "undefined") options.error(data) 24 | } 25 | } 26 | } 27 | const query = Object.assign({}, options.query) 28 | req.open(options.method, options.url + "?" + Object.keys(query).map(k => encodeURIComponent(k) + '=' + encodeURIComponent(query[k])).join('&'), true) 29 | req.send(options.payload) 30 | }, 31 | appendSorted: function(rootSelector, data, map) { 32 | // Find proper key 33 | let key; 34 | for (let k in map) { 35 | if (map.hasOwnProperty(k)) { 36 | if (map[k].name > data.name && (typeof key === "undefined" || map[key].name > map[k].name)) { 37 | key = k; 38 | break; 39 | } 40 | } 41 | } 42 | 43 | // Update html 44 | if (typeof key !== "undefined") { 45 | rootSelector.insertBefore(data.html.wrapper, map[key].html.wrapper); 46 | } else { 47 | rootSelector.append(data.html.wrapper); 48 | } 49 | }, 50 | removeClass: function(node, name) { 51 | // Get class name funcs 52 | let classNameFuncs = this.classNameFuncs(node) 53 | 54 | // No class name funcs 55 | if (!classNameFuncs) return 56 | 57 | // Remove 58 | let names = classNameFuncs[0]().split(" ") 59 | for (let idx = 0; idx < names.length; idx++) { 60 | if (names[idx] === name) { 61 | names.splice(idx, 1) 62 | idx-- 63 | } 64 | } 65 | 66 | // Set class name 67 | classNameFuncs[1](names.join(" ")) 68 | }, 69 | addClass: function(node, name) { 70 | // Get class name funcs 71 | let classNameFuncs = this.classNameFuncs(node) 72 | 73 | // No class name funcs 74 | if (!classNameFuncs) return 75 | 76 | // Set class name 77 | classNameFuncs[1](classNameFuncs[0]() + " " + name) 78 | }, 79 | classNameFuncs: function(node) { 80 | switch (typeof node.className) { 81 | case "string": 82 | return [ 83 | function() { 84 | return node.className 85 | }, 86 | function(name) { 87 | node.className = name 88 | }, 89 | ] 90 | case "object": 91 | switch (node.className.constructor.name) { 92 | case "SVGAnimatedString": 93 | return [ 94 | function() { 95 | return node.className.baseVal 96 | }, 97 | function(name) { 98 | node.className.baseVal = name 99 | } 100 | ] 101 | default: 102 | return false 103 | } 104 | default: 105 | return false 106 | } 107 | }, 108 | scrollDownTo: function(y, maxDuration) { 109 | if (typeof maxDuration === "undefined") maxDuration = 500 110 | let previousAt = (new Date()).getTime() 111 | const intervalDuration = 5 112 | const intervalScroll = (y - window.scrollY) / (maxDuration / intervalDuration) 113 | const i = setInterval(function() { 114 | if (window.scrollY >= y) { 115 | clearInterval(i) 116 | return 117 | } 118 | const now = (new Date()).getTime() 119 | const n = Math.floor((now - previousAt) / intervalDuration) 120 | previousAt = now 121 | window.scrollTo(0, window.scrollY + Math.min(n*intervalScroll, y-window.scrollY)) 122 | }, intervalDuration) 123 | }, 124 | isEmail: function(text) { 125 | return /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(text) 126 | } 127 | } -------------------------------------------------------------------------------- /resources/app/lib/js/astiws/astiws.js: -------------------------------------------------------------------------------- 1 | if (typeof asticode === "undefined") { 2 | var asticode = {}; 3 | } 4 | asticode.ws = { 5 | init: function(options) { 6 | const self = this 7 | 8 | if (!self.windowUnloadHandled) { 9 | window.onbeforeunload = function() { 10 | self.showOfflineMessage = false; 11 | if (typeof self.s !== "undefined") { 12 | self.s.close(); 13 | } 14 | } 15 | self.windowUnloadHandled = true 16 | } 17 | 18 | const okRequest = options.okRequest 19 | okRequest.error = function() { 20 | setTimeout(function() { 21 | self.init(options) 22 | }, 1000) 23 | } 24 | okRequest.success = function() { 25 | // Init websocket 26 | const query = Object.assign({}, options.query) 27 | self.s = new WebSocket(options.url + "?" + Object.keys(query).map(k => encodeURIComponent(k) + '=' + encodeURIComponent(query[k])).join('&')) 28 | 29 | // Declare functions 30 | let intervalPing 31 | self.s.onclose = function() { 32 | if (self.showOfflineMessage) { 33 | self.showOfflineMessage = false 34 | if (typeof options.offline !== "undefined") options.offline() 35 | } 36 | clearInterval(intervalPing) 37 | setTimeout(function() { 38 | self.init(options) 39 | }, 1000) 40 | } 41 | self.s.onopen = function() { 42 | self.showOfflineMessage = true 43 | if (typeof options.pingPeriod !== "undefined") { 44 | let pingFunc = options.pingFunc 45 | if (typeof pingFunc === "undefined") { 46 | pingFunc = function(self) { self.send("ping") } 47 | } 48 | intervalPing = setInterval(function() { pingFunc(self) }, options.pingPeriod / 1e6) 49 | } 50 | if (typeof options.open !== "undefined") options.open() 51 | } 52 | self.s.onmessage = function(event) { 53 | let data = JSON.parse(event.data) 54 | if (typeof options.message !== "undefined") options.message(data.event_name, data.payload) 55 | if (typeof options.messageRaw !== "undefined") options.messageRaw(data) 56 | } 57 | } 58 | 59 | asticode.tools.sendHttp(okRequest) 60 | }, 61 | send: function(event_name, payload) { 62 | this.sendJSON({event_name: event_name, payload: payload}) 63 | }, 64 | sendJSON: function(data) { 65 | this.s.send(JSON.stringify(data)) 66 | } 67 | } -------------------------------------------------------------------------------- /resources/app/lib/js/tabulator/jquery_wrapper.js: -------------------------------------------------------------------------------- 1 | /* Tabulator v4.7.1 (c) Oliver Folkerd */ 2 | 3 | /* 4 | * This file is part of the Tabulator package. 5 | * 6 | * (c) Oliver Folkerd 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * Full Documentation & Demos can be found at: http://olifolkerd.github.io/tabulator/ 12 | * 13 | */ 14 | 15 | (function (factory) { 16 | "use strict"; 17 | 18 | if (typeof define === 'function' && define.amd) { 19 | define(['jquery', 'resources/app/lib/js/tabulator/tabulator', 'jquery-ui'], factory); 20 | } else if (typeof module !== 'undefined' && module.exports) { 21 | module.exports = factory(require('jquery'), require('resources/app/lib/js/tabulator/tabulator'), require('jquery-ui')); 22 | } else { 23 | factory(jQuery, Tabulator); 24 | } 25 | })(function ($, Tabulator) { 26 | 27 | $.widget("ui.tabulator", { 28 | _create: function _create() { 29 | var options = Object.assign({}, this.options); 30 | 31 | delete options.create; 32 | delete options.disabled; 33 | 34 | this.table = new Tabulator(this.element[0], options); 35 | 36 | //map tabulator functions to jquery wrapper 37 | for (var key in Tabulator.prototype) { 38 | if (typeof Tabulator.prototype[key] === "function" && key.charAt(0) !== "_") { 39 | this[key] = this.table[key].bind(this.table); 40 | } 41 | } 42 | }, 43 | 44 | _setOption: function _setOption(option, value) { 45 | console.error("Tabulator jQuery wrapper does not support setting options after the table has been instantiated"); 46 | }, 47 | 48 | _destroy: function _destroy(option, value) { 49 | this.table.destroy(); 50 | } 51 | }); 52 | }); -------------------------------------------------------------------------------- /resources/app/lib/js/tabulator/jquery_wrapper.min.js: -------------------------------------------------------------------------------- 1 | /* Tabulator v4.7.1 (c) Oliver Folkerd */ 2 | !function(e){"use strict";"function"==typeof define&&define.amd?define(["jquery",`resources`,"jquery-ui"],e):"undefined"!=typeof module&&module.exports?module.exports=e(require("jquery"),require(`resources`),require("jquery-ui")):e(jQuery,Tabulator)}(function(e, t){e.widget("ui.tabulator",{_create:function(){var e=Object.assign({},this.options);delete e.create,delete e.disabled,this.table=new t(this.element[0],e);for(var o in t.prototype)"function"==typeof t.prototype[o]&&"_"!==o.charAt(0)&&(this[o]=this.table[o].bind(this.table))},_setOption:function(e, t){console.error("Tabulator jQuery wrapper does not support setting options after the table has been instantiated")},_destroy:function(e, t){this.table.destroy()}})}); -------------------------------------------------------------------------------- /resources/app/lib/js/tabulator/modules/accessor.js: -------------------------------------------------------------------------------- 1 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; 2 | 3 | /* Tabulator v4.7.1 (c) Oliver Folkerd */ 4 | 5 | var Accessor = function Accessor(table) { 6 | this.table = table; //hold Tabulator object 7 | this.allowedTypes = ["", "data", "download", "clipboard", "print", "htmlOutput"]; //list of accessor types 8 | }; 9 | 10 | //initialize column accessor 11 | Accessor.prototype.initializeColumn = function (column) { 12 | var self = this, 13 | match = false, 14 | config = {}; 15 | 16 | this.allowedTypes.forEach(function (type) { 17 | var key = "accessor" + (type.charAt(0).toUpperCase() + type.slice(1)), 18 | accessor; 19 | 20 | if (column.definition[key]) { 21 | accessor = self.lookupAccessor(column.definition[key]); 22 | 23 | if (accessor) { 24 | match = true; 25 | 26 | config[key] = { 27 | accessor: accessor, 28 | params: column.definition[key + "Params"] || {} 29 | }; 30 | } 31 | } 32 | }); 33 | 34 | if (match) { 35 | column.modules.accessor = config; 36 | } 37 | }; 38 | 39 | Accessor.prototype.lookupAccessor = function (value) { 40 | var accessor = false; 41 | 42 | //set column accessor 43 | switch (typeof value === "undefined" ? "undefined" : _typeof(value)) { 44 | case "string": 45 | if (this.accessors[value]) { 46 | accessor = this.accessors[value]; 47 | } else { 48 | console.warn("Accessor Error - No such accessor found, ignoring: ", value); 49 | } 50 | break; 51 | 52 | case "function": 53 | accessor = value; 54 | break; 55 | } 56 | 57 | return accessor; 58 | }; 59 | 60 | //apply accessor to row 61 | Accessor.prototype.transformRow = function (dataIn, type) { 62 | var self = this, 63 | key = "accessor" + (type.charAt(0).toUpperCase() + type.slice(1)); 64 | 65 | //clone data object with deep copy to isolate internal data from returned result 66 | var data = Tabulator.prototype.helpers.deepClone(dataIn || {}); 67 | 68 | self.table.columnManager.traverse(function (column) { 69 | var value, accessor, params, component; 70 | 71 | if (column.modules.accessor) { 72 | 73 | accessor = column.modules.accessor[key] || column.modules.accessor.accessor || false; 74 | 75 | if (accessor) { 76 | value = column.getFieldValue(data); 77 | 78 | if (value != "undefined") { 79 | component = column.getComponent(); 80 | params = typeof accessor.params === "function" ? accessor.params(value, data, type, component) : accessor.params; 81 | column.setFieldValue(data, accessor.accessor(value, data, type, params, component)); 82 | } 83 | } 84 | } 85 | }); 86 | 87 | return data; 88 | }, 89 | 90 | //default accessors 91 | Accessor.prototype.accessors = {}; 92 | 93 | Tabulator.prototype.registerModule("accessor", Accessor); -------------------------------------------------------------------------------- /resources/app/lib/js/tabulator/modules/accessor.min.js: -------------------------------------------------------------------------------- 1 | /* Tabulator v4.7.1 (c) Oliver Folkerd */ 2 | var _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(o){return typeof o}:function(o){return o&&"function"==typeof Symbol&&o.constructor===Symbol&&o!==Symbol.prototype?"symbol":typeof o},Accessor=function(o){this.table=o,this.allowedTypes=["","data","download","clipboard","print","htmlOutput"]};Accessor.prototype.initializeColumn=function(o){var e=this,s=!1,r={};this.allowedTypes.forEach(function(t){var c,a="accessor"+(t.charAt(0).toUpperCase()+t.slice(1));o.definition[a]&&(c=e.lookupAccessor(o.definition[a]))&&(s=!0,r[a]={accessor:c,params:o.definition[a+"Params"]||{}})}),s&&(o.modules.accessor=r)},Accessor.prototype.lookupAccessor=function(o){var e=!1;switch(void 0===o?"undefined":_typeof(o)){case"string":this.accessors[o]?e=this.accessors[o]:console.warn("Accessor Error - No such accessor found, ignoring: ",o);break;case"function":e=o}return e},Accessor.prototype.transformRow=function(o,e){var s=this,r="accessor"+(e.charAt(0).toUpperCase()+e.slice(1)),t=Tabulator.prototype.helpers.deepClone(o||{});return s.table.columnManager.traverse(function(o){var s,c,a,n;o.modules.accessor&&(c=o.modules.accessor[r]||o.modules.accessor.accessor||!1)&&"undefined"!=(s=o.getFieldValue(t))&&(n=o.getComponent(),a="function"==typeof c.params?c.params(s,t,e,n):c.params,o.setFieldValue(t,c.accessor(s,t,e,a,n)))}),t},Accessor.prototype.accessors={},Tabulator.prototype.registerModule("accessor",Accessor); -------------------------------------------------------------------------------- /resources/app/lib/js/tabulator/modules/clipboard.min.js: -------------------------------------------------------------------------------- 1 | /* Tabulator v4.7.1 (c) Oliver Folkerd */ 2 | var _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Clipboard=function(t){this.table=t,this.mode=!0,this.pasteParser=function(){},this.pasteAction=function(){},this.customSelection=!1,this.rowRange=!1,this.blocked=!0};Clipboard.prototype.initialize=function(){var t=this;this.mode=this.table.options.clipboard,this.rowRange=this.table.options.clipboardCopyRowRange,!0!==this.mode&&"copy"!==this.mode||this.table.element.addEventListener("copy",function(e){var a,o,i;if(!t.blocked){if(e.preventDefault(),t.customSelection)a=t.customSelection,t.table.options.clipboardCopyFormatter&&(a=t.table.options.clipboardCopyFormatter("plain",a));else{var i=t.table.modules.export.generateExportList(t.rowRange,t.table.options.clipboardCopyStyled,t.table.options.clipboardCopyConfig,"clipboard");o=t.table.modules.export.genereateHTMLTable(i),a=o?t.generatePlainContent(i):"",t.table.options.clipboardCopyFormatter&&(a=t.table.options.clipboardCopyFormatter("plain",a),o=t.table.options.clipboardCopyFormatter("html",o))}window.clipboardData&&window.clipboardData.setData?window.clipboardData.setData("Text",a):e.clipboardData&&e.clipboardData.setData?(e.clipboardData.setData("text/plain",a),o&&e.clipboardData.setData("text/html",o)):e.originalEvent&&e.originalEvent.clipboardData.setData&&(e.originalEvent.clipboardData.setData("text/plain",a),o&&e.originalEvent.clipboardData.setData("text/html",o)),t.table.options.clipboardCopied.call(t.table,a,o),t.reset()}}),!0!==this.mode&&"paste"!==this.mode||this.table.element.addEventListener("paste",function(e){t.paste(e)}),this.setPasteParser(this.table.options.clipboardPasteParser),this.setPasteAction(this.table.options.clipboardPasteAction)},Clipboard.prototype.reset=function(){this.blocked=!1,this.originalSelectionText=""},Clipboard.prototype.generatePlainContent=function(t){var e=[];return t.forEach(function(t){var a=[];t.columns.forEach(function(e){var o="";if(e)switch("group"===t.type&&(e.value=e.component.getKey()),_typeof(e.value)){case"object":o=JSON.stringify(e.value);break;case"undefined":case"null":o="";break;default:o=e.value}a.push(o)}),e.push(a.join("\t"))}),e.join("\n")},Clipboard.prototype.copy=function(t,e){var t,a,o;this.blocked=!1,this.customSelection=!1,!0!==this.mode&&"copy"!==this.mode||(this.rowRange=t||this.table.options.clipboardCopyRowRange,void 0!==window.getSelection&&void 0!==document.createRange?(t=document.createRange(),t.selectNodeContents(this.table.element),a=window.getSelection(),a.toString()&&e&&(this.customSelection=a.toString()),a.removeAllRanges(),a.addRange(t)):void 0!==document.selection&&void 0!==document.body.createTextRange&&(o=document.body.createTextRange(),o.moveToElementText(this.table.element),o.select()),document.execCommand("copy"),a&&a.removeAllRanges())},Clipboard.prototype.setPasteAction=function(t){switch(void 0===t?"undefined":_typeof(t)){case"string":this.pasteAction=this.pasteActions[t],this.pasteAction||console.warn("Clipboard Error - No such paste action found:",t);break;case"function":this.pasteAction=t}},Clipboard.prototype.setPasteParser=function(t){switch(void 0===t?"undefined":_typeof(t)){case"string":this.pasteParser=this.pasteParsers[t],this.pasteParser||console.warn("Clipboard Error - No such paste parser found:",t);break;case"function":this.pasteParser=t}},Clipboard.prototype.paste=function(t){var e,a,o;this.checkPaseOrigin(t)&&(e=this.getPasteData(t),a=this.pasteParser.call(this,e),a?(t.preventDefault(),this.table.modExists("mutator")&&(a=this.mutateData(a)),o=this.pasteAction.call(this,a),this.table.options.clipboardPasted.call(this.table,e,a,o)):this.table.options.clipboardPasteError.call(this.table,e))},Clipboard.prototype.mutateData=function(t){var e=this,a=[];return Array.isArray(t)?t.forEach(function(t){a.push(e.table.modules.mutator.transformRow(t,"clipboard"))}):a=t,a},Clipboard.prototype.checkPaseOrigin=function(t){var e=!0;return("DIV"!=t.target.tagName||this.table.modules.edit.currentCell)&&(e=!1),e},Clipboard.prototype.getPasteData=function(t){var e;return window.clipboardData&&window.clipboardData.getData?e=window.clipboardData.getData("Text"):t.clipboardData&&t.clipboardData.getData?e=t.clipboardData.getData("text/plain"):t.originalEvent&&t.originalEvent.clipboardData.getData&&(e=t.originalEvent.clipboardData.getData("text/plain")),e},Clipboard.prototype.pasteParsers={table:function(t){var e=[],a=!0,o=this.table.columnManager.columns,i=[],n=[];return t=t.split("\n"),t.forEach(function(t){e.push(t.split("\t"))}),!(!e.length||1===e.length&&e[0].length<2)&&(!0,e[0].forEach(function(t){var e=o.find(function(e){return t&&e.definition.title&&t.trim()&&e.definition.title.trim()===t.trim()});e?i.push(e):a=!1}),a||(a=!0,i=[],e[0].forEach(function(t){var e=o.find(function(e){return t&&e.field&&t.trim()&&e.field.trim()===t.trim()});e?i.push(e):a=!1}),a||(i=this.table.columnManager.columnsByIndex)),a&&e.shift(),e.forEach(function(t){var e={};t.forEach(function(t,a){i[a]&&(e[i[a].field]=t)}),n.push(e)}),n)}},Clipboard.prototype.pasteActions={replace:function(t){return this.table.setData(t)},update:function(t){return this.table.updateOrAddData(t)},insert:function(t){return this.table.addData(t)}},Tabulator.prototype.registerModule("clipboard",Clipboard); -------------------------------------------------------------------------------- /resources/app/lib/js/tabulator/modules/download.min.js: -------------------------------------------------------------------------------- 1 | /* Tabulator v4.7.1 (c) Oliver Folkerd */ 2 | var _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},Download=function(e){this.table=e};Download.prototype.download=function(e,o,t,n,a){function r(t,n){a?!0===a?l.triggerDownload(t,n,e,o,!0):a(t):l.triggerDownload(t,n,e,o)}var l=this,s=!1;if("function"==typeof e?s=e:l.downloaders[e]?s=l.downloaders[e]:console.warn("Download Error - No such download type found: ",e),s){var i=this.generateExportList(n);s.call(this.table,i,t||{},r)}},Download.prototype.generateExportList=function(e){var o=this.table.modules.export.generateExportList(this.table.options.downloadConfig,!1,e||this.table.options.downloadRowRange,"download"),t=this.table.options.groupHeaderDownload;return t&&!Array.isArray(t)&&(t=[t]),o.forEach(function(e){var o;"group"===e.type&&(o=e.columns[0],t&&t[e.indent]&&(o.value=t[e.indent](o.value,e.component._group.getRowCount(),e.component._group.getData(),e.component)))}),o},Download.prototype.triggerDownload=function(e,o,t,n,a){var r=document.createElement("a"),l=new Blob([e],{type:o}),n=n||"Tabulator."+("function"==typeof t?"txt":t);(l=this.table.options.downloadReady.call(this.table,e,l))&&(a?window.open(window.URL.createObjectURL(l)):navigator.msSaveOrOpenBlob?navigator.msSaveOrOpenBlob(l,n):(r.setAttribute("href",window.URL.createObjectURL(l)),r.setAttribute("download",n),r.style.display="none",document.body.appendChild(r),r.click(),document.body.removeChild(r)),this.table.options.downloadComplete&&this.table.options.downloadComplete())},Download.prototype.commsReceived=function(e,o,t){switch(o){case"intercept":this.download(t.type,"",t.options,t.active,t.intercept)}},Download.prototype.downloaders={csv:function(e,o,t){var n=o&&o.delimiter?o.delimiter:",",a=[],r=[];e.forEach(function(e){var o=[];switch(e.type){case"group":console.warn("Download Warning - CSV downloader cannot process row groups");break;case"calc":console.warn("Download Warning - CSV downloader cannot process column calculations");break;case"header":e.columns.forEach(function(e,o){e&&1===e.depth&&(r[o]=void 0===e.value||"null"==typeof e.value?"":e.value)});break;case"row":e.columns.forEach(function(e){if(e){switch(_typeof(e.value)){case"object":e.value=JSON.stringify(e.value);break;case"undefined":case"null":e.value=""}o.push('"'+String(e.value).split('"').join('""')+'"')}}),a.push(o.join(n))}}),r.length&&(a=[r].concat(a)),a=a.join("\n"),o.bom&&(a="\ufeff"+a),t(a,"text/csv")},json:function(e,o,t){var n=[];e.forEach(function(e){var o={};switch(e.type){case"header":break;case"group":console.warn("Download Warning - JSON downloader cannot process row groups");break;case"calc":console.warn("Download Warning - JSON downloader cannot process column calculations");break;case"row":e.columns.forEach(function(e){e&&(o[e.component.getField()]=e.value)}),n.push(o)}}),n=JSON.stringify(n,null,"\t"),t(n,"application/json")},pdf:function(e,o,t){function n(e,o){var t=[];return e.columns.forEach(function(e){var n;if(e){switch(_typeof(e.value)){case"object":e.value=JSON.stringify(e.value);break;case"undefined":case"null":e.value=""}n={content:e.value,colSpan:e.width,rowSpan:e.height},o&&(n.styles=o),t.push(n)}else t.push("")}),t}var a=[],r=[],l={},s=o.rowGroupStyles||{fontStyle:"bold",fontSize:12,cellPadding:6,fillColor:220},i=o.rowCalcStyles||{fontStyle:"bold",fontSize:10,cellPadding:4,fillColor:232},c=o.jsPDF||{},u=o&&o.title?o.title:"";c.orientation||(c.orientation=o.orientation||"landscape"),c.unit||(c.unit="pt"),e.forEach(function(e){switch(e.type){case"header":a.push(n(e));break;case"group":r.push(n(e,s));break;case"calc":r.push(n(e,i));break;case"row":r.push(n(e))}});var d=new jsPDF(c);o&&o.autoTable&&(l="function"==typeof o.autoTable?o.autoTable(d)||{}:o.autoTable),u&&(l.addPageContent=function(e){d.text(u,40,30)}),l.head=a,l.body=r,d.autoTable(l),o&&o.documentProcessing&&o.documentProcessing(d),t(d.output("arraybuffer"),"application/pdf")},xlsx:function(e,o,t){function n(){var o=[],t=[],n={},a={s:{c:0,r:0},e:{c:e[0]?e[0].columns.reduce(function(e,o){return e+(o&&o.width?o.width:1)},0):0,r:e.length}};return e.forEach(function(e,n){var a=[];e.columns.forEach(function(e,o){e?(a.push(e.value instanceof Date||"object"!==_typeof(e.value)?e.value:JSON.stringify(e.value)),(e.width>1||e.height>-1)&&t.push({s:{r:n,c:o},e:{r:n+e.height-1,c:o+e.width-1}})):a.push("")}),o.push(a)}),XLSX.utils.sheet_add_aoa(n,o),n["!ref"]=XLSX.utils.encode_range(a),t.length&&(n["!merges"]=t),n}var a,r=this,l=o.sheetName||"Sheet1",s=XLSX.utils.book_new();if(s.SheetNames=[],s.Sheets={},o.sheetOnly)return void t(n());if(o.sheets)for(var i in o.sheets)!0===o.sheets[i]?(s.SheetNames.push(i),s.Sheets[i]=n()):(s.SheetNames.push(i),this.table.modules.comms.send(o.sheets[i],"download","intercept",{type:"xlsx",options:{sheetOnly:!0},active:r.active,intercept:function(e){s.Sheets[i]=e}}));else s.SheetNames.push(l),s.Sheets[l]=n();o.documentProcessing&&(s=o.documentProcessing(s)),a=XLSX.write(s,{bookType:"xlsx",bookSST:!0,type:"binary"}),t(function(e){for(var o=new ArrayBuffer(e.length),t=new Uint8Array(o),n=0;n!=e.length;++n)t[n]=255&e.charCodeAt(n);return o}(a),"application/octet-stream")},html:function(e,o,t){this.modExists("export",!0)&&t(this.modules.export.genereateHTMLTable(e),"text/html")}},Tabulator.prototype.registerModule("download",Download); -------------------------------------------------------------------------------- /resources/app/lib/js/tabulator/modules/frozen_columns.min.js: -------------------------------------------------------------------------------- 1 | /* Tabulator v4.7.1 (c) Oliver Folkerd */ 2 | var FrozenColumns=function(t){this.table=t,this.leftColumns=[],this.rightColumns=[],this.leftMargin=0,this.rightMargin=0,this.rightPadding=0,this.initializationMode="left",this.active=!1,this.scrollEndTimer=!1};FrozenColumns.prototype.reset=function(){this.initializationMode="left",this.leftColumns=[],this.rightColumns=[],this.leftMargin=0,this.rightMargin=0,this.rightMargin=0,this.active=!1,this.table.columnManager.headersElement.style.marginLeft=0,this.table.columnManager.element.style.paddingRight=0},FrozenColumns.prototype.initializeColumn=function(t){var e={margin:0,edge:!1};t.isGroup||(this.frozenCheck(t)?(e.position=this.initializationMode,"left"==this.initializationMode?this.leftColumns.push(t):this.rightColumns.unshift(t),this.active=!0,t.modules.frozen=e):this.initializationMode="right")},FrozenColumns.prototype.frozenCheck=function(t){return t.parent.isGroup&&t.definition.frozen&&console.warn("Frozen Column Error - Parent column group must be frozen, not individual columns or sub column groups"),t.parent.isGroup?this.frozenCheck(t.parent):t.definition.frozen},FrozenColumns.prototype.scrollHorizontal=function(){var t,e=this;this.active&&(clearTimeout(this.scrollEndTimer),this.scrollEndTimer=setTimeout(function(){e.layout()},100),t=this.table.rowManager.getVisibleRows(),this.calcMargins(),this.layoutColumnPosition(),this.layoutCalcRows(),t.forEach(function(t){"row"===t.type&&e.layoutRow(t)}),this.table.rowManager.tableElement.style.marginRight=this.rightMargin)},FrozenColumns.prototype.calcMargins=function(){this.leftMargin=this._calcSpace(this.leftColumns,this.leftColumns.length)+"px",this.table.columnManager.headersElement.style.marginLeft=this.leftMargin,this.rightMargin=this._calcSpace(this.rightColumns,this.rightColumns.length)+"px",this.table.columnManager.element.style.paddingRight=this.rightMargin,this.rightPadding=this.table.rowManager.element.clientWidth+this.table.columnManager.scrollLeft},FrozenColumns.prototype.layoutCalcRows=function(){this.table.modExists("columnCalcs")&&(this.table.modules.columnCalcs.topInitialized&&this.table.modules.columnCalcs.topRow&&this.layoutRow(this.table.modules.columnCalcs.topRow),this.table.modules.columnCalcs.botInitialized&&this.table.modules.columnCalcs.botRow&&this.layoutRow(this.table.modules.columnCalcs.botRow))},FrozenColumns.prototype.layoutColumnPosition=function(t){var e=this,o=[];this.leftColumns.forEach(function(n,l){if(n.modules.frozen.margin=e._calcSpace(e.leftColumns,l)+e.table.columnManager.scrollLeft+"px",l==e.leftColumns.length-1?n.modules.frozen.edge=!0:n.modules.frozen.edge=!1,n.parent.isGroup){var i=e.getColGroupParentElement(n);o.includes(i)||(e.layoutElement(i,n),o.push(i)),n.modules.frozen.edge&&i.classList.add("tabulator-frozen-"+n.modules.frozen.position)}else e.layoutElement(n.getElement(),n);t&&n.cells.forEach(function(t){e.layoutElement(t.getElement(),n)})}),this.rightColumns.forEach(function(o,n){o.modules.frozen.margin=e.rightPadding-e._calcSpace(e.rightColumns,n+1)+"px",n==e.rightColumns.length-1?o.modules.frozen.edge=!0:o.modules.frozen.edge=!1,o.parent.isGroup?e.layoutElement(e.getColGroupParentElement(o),o):e.layoutElement(o.getElement(),o),t&&o.cells.forEach(function(t){e.layoutElement(t.getElement(),o)})})},FrozenColumns.prototype.getColGroupParentElement=function(t){return t.parent.isGroup?this.getColGroupParentElement(t.parent):t.getElement()},FrozenColumns.prototype.layout=function(){var t=this;t.active&&(this.calcMargins(),t.table.rowManager.getDisplayRows().forEach(function(e){"row"===e.type&&t.layoutRow(e)}),this.layoutCalcRows(),this.layoutColumnPosition(!0),this.table.rowManager.tableElement.style.marginRight=this.rightMargin)},FrozenColumns.prototype.layoutRow=function(t){var e=this;t.getElement().style.paddingLeft=this.leftMargin,this.leftColumns.forEach(function(o){var n=t.getCell(o);n&&e.layoutElement(n.getElement(),o)}),this.rightColumns.forEach(function(o){var n=t.getCell(o);n&&e.layoutElement(n.getElement(),o)})},FrozenColumns.prototype.layoutElement=function(t,e){e.modules.frozen&&(t.style.position="absolute",t.style.left=e.modules.frozen.margin,t.classList.add("tabulator-frozen"),e.modules.frozen.edge&&t.classList.add("tabulator-frozen-"+e.modules.frozen.position))},FrozenColumns.prototype._calcSpace=function(t,e){for(var o=0,n=0;n -1) { 41 | output.splice(index, 1); 42 | } 43 | }); 44 | 45 | return output; 46 | }; 47 | 48 | FrozenRows.prototype.freezeRow = function (row) { 49 | if (!row.modules.frozen) { 50 | row.modules.frozen = true; 51 | this.topElement.appendChild(row.getElement()); 52 | row.initialize(); 53 | row.normalizeHeight(); 54 | this.table.rowManager.adjustTableSize(); 55 | 56 | this.rows.push(row); 57 | 58 | this.table.rowManager.refreshActiveData("display"); 59 | 60 | this.styleRows(); 61 | } else { 62 | console.warn("Freeze Error - Row is already frozen"); 63 | } 64 | }; 65 | 66 | FrozenRows.prototype.unfreezeRow = function (row) { 67 | var index = this.rows.indexOf(row); 68 | 69 | if (row.modules.frozen) { 70 | 71 | row.modules.frozen = false; 72 | 73 | var rowEl = row.getElement(); 74 | rowEl.parentNode.removeChild(rowEl); 75 | 76 | this.table.rowManager.adjustTableSize(); 77 | 78 | this.rows.splice(index, 1); 79 | 80 | this.table.rowManager.refreshActiveData("display"); 81 | 82 | if (this.rows.length) { 83 | this.styleRows(); 84 | } 85 | } else { 86 | console.warn("Freeze Error - Row is already unfrozen"); 87 | } 88 | }; 89 | 90 | FrozenRows.prototype.styleRows = function (row) { 91 | var self = this; 92 | 93 | this.rows.forEach(function (row, i) { 94 | self.table.rowManager.styleRow(row, i); 95 | }); 96 | }; 97 | 98 | Tabulator.prototype.registerModule("frozenRows", FrozenRows); -------------------------------------------------------------------------------- /resources/app/lib/js/tabulator/modules/frozen_rows.min.js: -------------------------------------------------------------------------------- 1 | /* Tabulator v4.7.1 (c) Oliver Folkerd */ 2 | var FrozenRows=function(e){this.table=e,this.topElement=document.createElement("div"),this.rows=[],this.displayIndex=0};FrozenRows.prototype.initialize=function(){this.rows=[],this.topElement.classList.add("tabulator-frozen-rows-holder"),this.table.columnManager.getElement().insertBefore(this.topElement,this.table.columnManager.headersElement.nextSibling)},FrozenRows.prototype.setDisplayIndex=function(e){this.displayIndex=e},FrozenRows.prototype.getDisplayIndex=function(){return this.displayIndex},FrozenRows.prototype.isFrozen=function(){return!!this.rows.length},FrozenRows.prototype.getRows=function(e){var o=e.slice(0);return this.rows.forEach(function(e){var t=o.indexOf(e);t>-1&&o.splice(t,1)}),o},FrozenRows.prototype.freezeRow=function(e){e.modules.frozen?console.warn("Freeze Error - Row is already frozen"):(e.modules.frozen=!0,this.topElement.appendChild(e.getElement()),e.initialize(),e.normalizeHeight(),this.table.rowManager.adjustTableSize(),this.rows.push(e),this.table.rowManager.refreshActiveData("display"),this.styleRows())},FrozenRows.prototype.unfreezeRow=function(e){var o=this.rows.indexOf(e);if(e.modules.frozen){e.modules.frozen=!1;var t=e.getElement();t.parentNode.removeChild(t),this.table.rowManager.adjustTableSize(),this.rows.splice(o,1),this.table.rowManager.refreshActiveData("display"),this.rows.length&&this.styleRows()}else console.warn("Freeze Error - Row is already unfrozen")},FrozenRows.prototype.styleRows=function(e){var o=this;this.rows.forEach(function(e,t){o.table.rowManager.styleRow(e,t)})},Tabulator.prototype.registerModule("frozenRows",FrozenRows); -------------------------------------------------------------------------------- /resources/app/lib/js/tabulator/modules/history.js: -------------------------------------------------------------------------------- 1 | /* Tabulator v4.7.1 (c) Oliver Folkerd */ 2 | 3 | var History = function History(table) { 4 | this.table = table; //hold Tabulator object 5 | 6 | this.history = []; 7 | this.index = -1; 8 | }; 9 | 10 | History.prototype.clear = function () { 11 | this.history = []; 12 | this.index = -1; 13 | }; 14 | 15 | History.prototype.action = function (type, component, data) { 16 | 17 | this.history = this.history.slice(0, this.index + 1); 18 | 19 | this.history.push({ 20 | type: type, 21 | component: component, 22 | data: data 23 | }); 24 | 25 | this.index++; 26 | }; 27 | 28 | History.prototype.getHistoryUndoSize = function () { 29 | return this.index + 1; 30 | }; 31 | 32 | History.prototype.getHistoryRedoSize = function () { 33 | return this.history.length - (this.index + 1); 34 | }; 35 | 36 | History.prototype.undo = function () { 37 | 38 | if (this.index > -1) { 39 | var action = this.history[this.index]; 40 | 41 | this.undoers[action.type].call(this, action); 42 | 43 | this.index--; 44 | 45 | this.table.options.historyUndo.call(this.table, action.type, action.component.getComponent(), action.data); 46 | 47 | return true; 48 | } else { 49 | console.warn("History Undo Error - No more history to undo"); 50 | return false; 51 | } 52 | }; 53 | 54 | History.prototype.redo = function () { 55 | if (this.history.length - 1 > this.index) { 56 | 57 | this.index++; 58 | 59 | var action = this.history[this.index]; 60 | 61 | this.redoers[action.type].call(this, action); 62 | 63 | this.table.options.historyRedo.call(this.table, action.type, action.component.getComponent(), action.data); 64 | 65 | return true; 66 | } else { 67 | console.warn("History Redo Error - No more history to redo"); 68 | return false; 69 | } 70 | }; 71 | 72 | History.prototype.undoers = { 73 | cellEdit: function cellEdit(action) { 74 | action.component.setValueProcessData(action.data.oldValue); 75 | }, 76 | 77 | rowAdd: function rowAdd(action) { 78 | action.component.deleteActual(); 79 | }, 80 | 81 | rowDelete: function rowDelete(action) { 82 | var newRow = this.table.rowManager.addRowActual(action.data.data, action.data.pos, action.data.index); 83 | 84 | if (this.table.options.groupBy && this.table.modExists("groupRows")) { 85 | this.table.modules.groupRows.updateGroupRows(true); 86 | } 87 | 88 | this._rebindRow(action.component, newRow); 89 | }, 90 | 91 | rowMove: function rowMove(action) { 92 | this.table.rowManager.moveRowActual(action.component, this.table.rowManager.rows[action.data.posFrom], !action.data.after); 93 | this.table.rowManager.redraw(); 94 | } 95 | }; 96 | 97 | History.prototype.redoers = { 98 | cellEdit: function cellEdit(action) { 99 | action.component.setValueProcessData(action.data.newValue); 100 | }, 101 | 102 | rowAdd: function rowAdd(action) { 103 | var newRow = this.table.rowManager.addRowActual(action.data.data, action.data.pos, action.data.index); 104 | 105 | if (this.table.options.groupBy && this.table.modExists("groupRows")) { 106 | this.table.modules.groupRows.updateGroupRows(true); 107 | } 108 | 109 | this._rebindRow(action.component, newRow); 110 | }, 111 | 112 | rowDelete: function rowDelete(action) { 113 | action.component.deleteActual(); 114 | }, 115 | 116 | rowMove: function rowMove(action) { 117 | this.table.rowManager.moveRowActual(action.component, this.table.rowManager.rows[action.data.posTo], action.data.after); 118 | this.table.rowManager.redraw(); 119 | } 120 | }; 121 | 122 | //rebind rows to new element after deletion 123 | History.prototype._rebindRow = function (oldRow, newRow) { 124 | this.history.forEach(function (action) { 125 | if (action.component instanceof Row) { 126 | if (action.component === oldRow) { 127 | action.component = newRow; 128 | } 129 | } else if (action.component instanceof Cell) { 130 | if (action.component.row === oldRow) { 131 | var field = action.component.column.getField(); 132 | 133 | if (field) { 134 | action.component = newRow.getCell(field); 135 | } 136 | } 137 | } 138 | }); 139 | }; 140 | 141 | Tabulator.prototype.registerModule("history", History); -------------------------------------------------------------------------------- /resources/app/lib/js/tabulator/modules/history.min.js: -------------------------------------------------------------------------------- 1 | /* Tabulator v4.7.1 (c) Oliver Folkerd */ 2 | var History=function(t){this.table=t,this.history=[],this.index=-1};History.prototype.clear=function(){this.history=[],this.index=-1},History.prototype.action=function(t,o,e){this.history=this.history.slice(0,this.index+1),this.history.push({type:t,component:o,data:e}),this.index++},History.prototype.getHistoryUndoSize=function(){return this.index+1},History.prototype.getHistoryRedoSize=function(){return this.history.length-(this.index+1)},History.prototype.undo=function(){if(this.index>-1){var t=this.history[this.index];return this.undoers[t.type].call(this,t),this.index--,this.table.options.historyUndo.call(this.table,t.type,t.component.getComponent(),t.data),!0}return console.warn("History Undo Error - No more history to undo"),!1},History.prototype.redo=function(){if(this.history.length-1>this.index){this.index++;var t=this.history[this.index];return this.redoers[t.type].call(this,t),this.table.options.historyRedo.call(this.table,t.type,t.component.getComponent(),t.data),!0}return console.warn("History Redo Error - No more history to redo"),!1},History.prototype.undoers={cellEdit:function(t){t.component.setValueProcessData(t.data.oldValue)},rowAdd:function(t){t.component.deleteActual()},rowDelete:function(t){var o=this.table.rowManager.addRowActual(t.data.data,t.data.pos,t.data.index);this.table.options.groupBy&&this.table.modExists("groupRows")&&this.table.modules.groupRows.updateGroupRows(!0),this._rebindRow(t.component,o)},rowMove:function(t){this.table.rowManager.moveRowActual(t.component,this.table.rowManager.rows[t.data.posFrom],!t.data.after),this.table.rowManager.redraw()}},History.prototype.redoers={cellEdit:function(t){t.component.setValueProcessData(t.data.newValue)},rowAdd:function(t){var o=this.table.rowManager.addRowActual(t.data.data,t.data.pos,t.data.index);this.table.options.groupBy&&this.table.modExists("groupRows")&&this.table.modules.groupRows.updateGroupRows(!0),this._rebindRow(t.component,o)},rowDelete:function(t){t.component.deleteActual()},rowMove:function(t){this.table.rowManager.moveRowActual(t.component,this.table.rowManager.rows[t.data.posTo],t.data.after),this.table.rowManager.redraw()}},History.prototype._rebindRow=function(t,o){this.history.forEach(function(e){if(e.component instanceof Row)e.component===t&&(e.component=o);else if(e.component instanceof Cell&&e.component.row===t){var i=e.component.column.getField();i&&(e.component=o.getCell(i))}})},Tabulator.prototype.registerModule("history",History); -------------------------------------------------------------------------------- /resources/app/lib/js/tabulator/modules/html_table_import.js: -------------------------------------------------------------------------------- 1 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; 2 | 3 | /* Tabulator v4.7.1 (c) Oliver Folkerd */ 4 | 5 | var HtmlTableImport = function HtmlTableImport(table) { 6 | this.table = table; //hold Tabulator object 7 | this.fieldIndex = []; 8 | this.hasIndex = false; 9 | }; 10 | 11 | HtmlTableImport.prototype.parseTable = function () { 12 | var self = this, 13 | element = self.table.element, 14 | options = self.table.options, 15 | columns = options.columns, 16 | headers = element.getElementsByTagName("th"), 17 | rows = element.getElementsByTagName("tbody")[0], 18 | data = [], 19 | newTable; 20 | 21 | self.hasIndex = false; 22 | 23 | self.table.options.htmlImporting.call(this.table); 24 | 25 | rows = rows ? rows.getElementsByTagName("tr") : []; 26 | 27 | //check for tablator inline options 28 | self._extractOptions(element, options); 29 | 30 | if (headers.length) { 31 | self._extractHeaders(headers, rows); 32 | } else { 33 | self._generateBlankHeaders(headers, rows); 34 | } 35 | 36 | //iterate through table rows and build data set 37 | for (var index = 0; index < rows.length; index++) { 38 | var row = rows[index], 39 | cells = row.getElementsByTagName("td"), 40 | item = {}; 41 | 42 | //create index if the dont exist in table 43 | if (!self.hasIndex) { 44 | item[options.index] = index; 45 | } 46 | 47 | for (var i = 0; i < cells.length; i++) { 48 | var cell = cells[i]; 49 | if (typeof this.fieldIndex[i] !== "undefined") { 50 | item[this.fieldIndex[i]] = cell.innerHTML; 51 | } 52 | } 53 | 54 | //add row data to item 55 | data.push(item); 56 | } 57 | 58 | //create new element 59 | var newElement = document.createElement("div"); 60 | 61 | //transfer attributes to new element 62 | var attributes = element.attributes; 63 | 64 | // loop through attributes and apply them on div 65 | 66 | for (var i in attributes) { 67 | if (_typeof(attributes[i]) == "object") { 68 | newElement.setAttribute(attributes[i].name, attributes[i].value); 69 | } 70 | } 71 | 72 | // replace table with div element 73 | element.parentNode.replaceChild(newElement, element); 74 | 75 | options.data = data; 76 | 77 | self.table.options.htmlImported.call(this.table); 78 | 79 | // // newElement.tabulator(options); 80 | 81 | this.table.element = newElement; 82 | }; 83 | 84 | //extract tabulator attribute options 85 | HtmlTableImport.prototype._extractOptions = function (element, options, defaultOptions) { 86 | var attributes = element.attributes; 87 | var optionsArr = defaultOptions ? Object.assign([], defaultOptions) : Object.keys(options); 88 | var optionsList = {}; 89 | 90 | optionsArr.forEach(function (item) { 91 | optionsList[item.toLowerCase()] = item; 92 | }); 93 | 94 | for (var index in attributes) { 95 | var attrib = attributes[index]; 96 | var name; 97 | 98 | if (attrib && (typeof attrib === "undefined" ? "undefined" : _typeof(attrib)) == "object" && attrib.name && attrib.name.indexOf("tabulator-") === 0) { 99 | name = attrib.name.replace("tabulator-", ""); 100 | 101 | if (typeof optionsList[name] !== "undefined") { 102 | options[optionsList[name]] = this._attribValue(attrib.value); 103 | } 104 | } 105 | } 106 | }; 107 | 108 | //get value of attribute 109 | HtmlTableImport.prototype._attribValue = function (value) { 110 | if (value === "true") { 111 | return true; 112 | } 113 | 114 | if (value === "false") { 115 | return false; 116 | } 117 | 118 | return value; 119 | }; 120 | 121 | //find column if it has already been defined 122 | HtmlTableImport.prototype._findCol = function (title) { 123 | var match = this.table.options.columns.find(function (column) { 124 | return column.title === title; 125 | }); 126 | 127 | return match || false; 128 | }; 129 | 130 | //extract column from headers 131 | HtmlTableImport.prototype._extractHeaders = function (headers, rows) { 132 | for (var index = 0; index < headers.length; index++) { 133 | var header = headers[index], 134 | exists = false, 135 | col = this._findCol(header.textContent), 136 | width, 137 | attributes; 138 | 139 | if (col) { 140 | exists = true; 141 | } else { 142 | col = { title: header.textContent.trim() }; 143 | } 144 | 145 | if (!col.field) { 146 | col.field = header.textContent.trim().toLowerCase().replace(" ", "_"); 147 | } 148 | 149 | width = header.getAttribute("width"); 150 | 151 | if (width && !col.width) { 152 | col.width = width; 153 | } 154 | 155 | //check for tablator inline options 156 | attributes = header.attributes; 157 | 158 | // //check for tablator inline options 159 | this._extractOptions(header, col, Column.prototype.defaultOptionList); 160 | 161 | this.fieldIndex[index] = col.field; 162 | 163 | if (col.field == this.table.options.index) { 164 | this.hasIndex = true; 165 | } 166 | 167 | if (!exists) { 168 | this.table.options.columns.push(col); 169 | } 170 | } 171 | }; 172 | 173 | //generate blank headers 174 | HtmlTableImport.prototype._generateBlankHeaders = function (headers, rows) { 175 | for (var index = 0; index < headers.length; index++) { 176 | var header = headers[index], 177 | col = { title: "", field: "col" + index }; 178 | 179 | this.fieldIndex[index] = col.field; 180 | 181 | var width = header.getAttribute("width"); 182 | 183 | if (width) { 184 | col.width = width; 185 | } 186 | 187 | this.table.options.columns.push(col); 188 | } 189 | }; 190 | 191 | Tabulator.prototype.registerModule("htmlTableImport", HtmlTableImport); -------------------------------------------------------------------------------- /resources/app/lib/js/tabulator/modules/html_table_import.min.js: -------------------------------------------------------------------------------- 1 | /* Tabulator v4.7.1 (c) Oliver Folkerd */ 2 | var _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},HtmlTableImport=function(t){this.table=t,this.fieldIndex=[],this.hasIndex=!1};HtmlTableImport.prototype.parseTable=function(){var t=this,e=t.table.element,o=t.table.options,a=(o.columns,e.getElementsByTagName("th")),n=e.getElementsByTagName("tbody")[0],l=[];t.hasIndex=!1,t.table.options.htmlImporting.call(this.table),n=n?n.getElementsByTagName("tr"):[],t._extractOptions(e,o),a.length?t._extractHeaders(a,n):t._generateBlankHeaders(a,n);for(var r=0;r-1&&t.pressedKeys.splice(n,1)}},this.table.element.addEventListener("keydown",this.keyupBinding),this.table.element.addEventListener("keyup",this.keydownBinding)},Keybindings.prototype.clearBindings=function(){this.keyupBinding&&this.table.element.removeEventListener("keydown",this.keyupBinding),this.keydownBinding&&this.table.element.removeEventListener("keyup",this.keydownBinding)},Keybindings.prototype.checkBinding=function(t,e){var i=this,n=!0;return t.ctrlKey==e.ctrl&&t.shiftKey==e.shift&&t.metaKey==e.meta&&(e.keys.forEach(function(t){-1==i.pressedKeys.indexOf(t)&&(n=!1)}),n&&e.action.call(i,t),!0)},Keybindings.prototype.bindings={navPrev:"shift + 9",navNext:9,navUp:38,navDown:40,scrollPageUp:33,scrollPageDown:34,scrollToStart:36,scrollToEnd:35,undo:"ctrl + 90",redo:"ctrl + 89",copyToClipboard:"ctrl + 67"},Keybindings.prototype.actions={keyBlock:function(t){t.stopPropagation(),t.preventDefault()},scrollPageUp:function(t){var e=this.table.rowManager,i=e.scrollTop-e.height;e.element.scrollHeight;t.preventDefault(),e.displayRowsCount&&(i>=0?e.element.scrollTop=i:e.scrollToRow(e.getDisplayRows()[0])),this.table.element.focus()},scrollPageDown:function(t){var e=this.table.rowManager,i=e.scrollTop+e.height,n=e.element.scrollHeight;t.preventDefault(),e.displayRowsCount&&(i<=n?e.element.scrollTop=i:e.scrollToRow(e.getDisplayRows()[e.displayRowsCount-1])),this.table.element.focus()},scrollToStart:function(t){var e=this.table.rowManager;t.preventDefault(),e.displayRowsCount&&e.scrollToRow(e.getDisplayRows()[0]),this.table.element.focus()},scrollToEnd:function(t){var e=this.table.rowManager;t.preventDefault(),e.displayRowsCount&&e.scrollToRow(e.getDisplayRows()[e.displayRowsCount-1]),this.table.element.focus()},navPrev:function(t){var e=!1;this.table.modExists("edit")&&(e=this.table.modules.edit.currentCell)&&(t.preventDefault(),e.nav().prev())},navNext:function(t){var e,i=!1,n=this.table.options.tabEndNewRow;this.table.modExists("edit")&&(i=this.table.modules.edit.currentCell)&&(t.preventDefault(),e=i.nav(),e.next()||n&&(i.getElement().firstChild.blur(),n=!0===n?this.table.addRow({}):"function"==typeof n?this.table.addRow(n(i.row.getComponent())):this.table.addRow(Object.assign({},n)),n.then(function(){setTimeout(function(){e.next()})})))},navLeft:function(t){var e=!1;this.table.modExists("edit")&&(e=this.table.modules.edit.currentCell)&&(t.preventDefault(),e.nav().left())},navRight:function(t){var e=!1;this.table.modExists("edit")&&(e=this.table.modules.edit.currentCell)&&(t.preventDefault(),e.nav().right())},navUp:function(t){var e=!1;this.table.modExists("edit")&&(e=this.table.modules.edit.currentCell)&&(t.preventDefault(),e.nav().up())},navDown:function(t){var e=!1;this.table.modExists("edit")&&(e=this.table.modules.edit.currentCell)&&(t.preventDefault(),e.nav().down())},undo:function(t){this.table.options.history&&this.table.modExists("history")&&this.table.modExists("edit")&&(this.table.modules.edit.currentCell||(t.preventDefault(),this.table.modules.history.undo()))},redo:function(t){this.table.options.history&&this.table.modExists("history")&&this.table.modExists("edit")&&(this.table.modules.edit.currentCell||(t.preventDefault(),this.table.modules.history.redo()))},copyToClipboard:function(t){this.table.modules.edit.currentCell||this.table.modExists("clipboard",!0)&&this.table.modules.clipboard.copy(!1,!0)}},Tabulator.prototype.registerModule("keybindings",Keybindings); -------------------------------------------------------------------------------- /resources/app/lib/js/tabulator/modules/menu.js: -------------------------------------------------------------------------------- 1 | /* Tabulator v4.7.1 (c) Oliver Folkerd */ 2 | 3 | var Menu = function Menu(table) { 4 | this.table = table; //hold Tabulator object 5 | this.menuEl = false; 6 | this.blurEvent = this.hideMenu.bind(this); 7 | this.escEvent = this.escMenu.bind(this); 8 | this.nestedMenuBlock = false; 9 | }; 10 | 11 | Menu.prototype.initializeColumnHeader = function (column) { 12 | var _this = this; 13 | 14 | var headerMenuEl; 15 | 16 | if (column.definition.headerContextMenu) { 17 | column.getElement().addEventListener("contextmenu", function (e) { 18 | var menu = typeof column.definition.headerContextMenu == "function" ? column.definition.headerContextMenu(column.getComponent()) : column.definition.headerContextMenu; 19 | 20 | e.preventDefault(); 21 | 22 | _this.loadMenu(e, column, menu); 23 | }); 24 | } 25 | 26 | if (column.definition.headerMenu) { 27 | 28 | headerMenuEl = document.createElement("span"); 29 | headerMenuEl.classList.add("tabulator-header-menu-button"); 30 | headerMenuEl.innerHTML = "⋮"; 31 | 32 | headerMenuEl.addEventListener("click", function (e) { 33 | var menu = typeof column.definition.headerMenu == "function" ? column.definition.headerMenu(column.getComponent()) : column.definition.headerMenu; 34 | e.stopPropagation(); 35 | e.preventDefault(); 36 | 37 | _this.loadMenu(e, column, menu); 38 | }); 39 | 40 | column.titleElement.insertBefore(headerMenuEl, column.titleElement.firstChild); 41 | } 42 | }; 43 | 44 | Menu.prototype.initializeCell = function (cell) { 45 | var _this2 = this; 46 | 47 | cell.getElement().addEventListener("contextmenu", function (e) { 48 | var menu = typeof cell.column.definition.contextMenu == "function" ? cell.column.definition.contextMenu(cell.getComponent()) : cell.column.definition.contextMenu; 49 | 50 | if (menu) { 51 | e.stopImmediatePropagation(); 52 | } 53 | 54 | _this2.loadMenu(e, cell, menu); 55 | }); 56 | }; 57 | 58 | Menu.prototype.initializeRow = function (row) { 59 | var _this3 = this; 60 | 61 | row.getElement().addEventListener("contextmenu", function (e) { 62 | var menu = typeof _this3.table.options.rowContextMenu == "function" ? _this3.table.options.rowContextMenu(row.getComponent()) : _this3.table.options.rowContextMenu; 63 | 64 | _this3.loadMenu(e, row, menu); 65 | }); 66 | }; 67 | 68 | Menu.prototype.initializeGroup = function (group) { 69 | var _this4 = this; 70 | 71 | group.getElement().addEventListener("contextmenu", function (e) { 72 | var menu = typeof _this4.table.options.groupContextMenu == "function" ? _this4.table.options.groupContextMenu(group.getComponent()) : _this4.table.options.groupContextMenu; 73 | 74 | _this4.loadMenu(e, group, menu); 75 | }); 76 | }; 77 | 78 | Menu.prototype.loadMenu = function (e, component, menu) { 79 | var _this5 = this; 80 | 81 | var docHeight = Math.max(document.body.offsetHeight, window.innerHeight); 82 | 83 | e.preventDefault(); 84 | 85 | //abort if no menu set 86 | if (!menu || !menu.length) { 87 | return; 88 | } 89 | 90 | if (this.nestedMenuBlock) { 91 | //abort if child menu already open 92 | if (this.isOpen()) { 93 | return; 94 | } 95 | } else { 96 | this.nestedMenuBlock = setTimeout(function () { 97 | _this5.nestedMenuBlock = false; 98 | }, 100); 99 | } 100 | 101 | this.hideMenu(); 102 | 103 | this.menuEl = document.createElement("div"); 104 | this.menuEl.classList.add("tabulator-menu"); 105 | 106 | menu.forEach(function (item) { 107 | var itemEl = document.createElement("div"); 108 | var label = item.label; 109 | var disabled = item.disabled; 110 | 111 | if (item.separator) { 112 | itemEl.classList.add("tabulator-menu-separator"); 113 | } else { 114 | itemEl.classList.add("tabulator-menu-item"); 115 | 116 | if (typeof label == "function") { 117 | label = label(component.getComponent()); 118 | } 119 | 120 | if (label instanceof Node) { 121 | itemEl.appendChild(label); 122 | } else { 123 | itemEl.innerHTML = label; 124 | } 125 | 126 | if (typeof disabled == "function") { 127 | disabled = disabled(component.getComponent()); 128 | } 129 | 130 | if (disabled) { 131 | itemEl.classList.add("tabulator-menu-item-disabled"); 132 | itemEl.addEventListener("click", function (e) { 133 | e.stopPropagation(); 134 | }); 135 | } else { 136 | itemEl.addEventListener("click", function (e) { 137 | _this5.hideMenu(); 138 | item.action(e, component.getComponent()); 139 | }); 140 | } 141 | } 142 | 143 | _this5.menuEl.appendChild(itemEl); 144 | }); 145 | 146 | this.menuEl.style.top = e.pageY + "px"; 147 | this.menuEl.style.left = e.pageX + "px"; 148 | 149 | document.body.addEventListener("click", this.blurEvent); 150 | this.table.rowManager.element.addEventListener("scroll", this.blurEvent); 151 | 152 | setTimeout(function () { 153 | document.body.addEventListener("contextmenu", _this5.blurEvent); 154 | }, 100); 155 | 156 | document.body.addEventListener("keydown", this.escEvent); 157 | 158 | document.body.appendChild(this.menuEl); 159 | 160 | //move menu to start on right edge if it is too close to the edge of the screen 161 | if (e.pageX + this.menuEl.offsetWidth >= document.body.offsetWidth) { 162 | this.menuEl.style.left = ""; 163 | this.menuEl.style.right = document.body.offsetWidth - e.pageX + "px"; 164 | } 165 | 166 | //move menu to start on bottom edge if it is too close to the edge of the screen 167 | if (e.pageY + this.menuEl.offsetHeight >= docHeight) { 168 | this.menuEl.style.top = ""; 169 | this.menuEl.style.bottom = docHeight - e.pageY + "px"; 170 | } 171 | }; 172 | 173 | Menu.prototype.isOpen = function () { 174 | return !!this.menuEl.parentNode; 175 | }; 176 | 177 | Menu.prototype.escMenu = function (e) { 178 | if (e.keyCode == 27) { 179 | this.hideMenu(); 180 | } 181 | }; 182 | 183 | Menu.prototype.hideMenu = function () { 184 | if (this.menuEl.parentNode) { 185 | this.menuEl.parentNode.removeChild(this.menuEl); 186 | } 187 | 188 | if (this.escEvent) { 189 | document.body.removeEventListener("keydown", this.escEvent); 190 | } 191 | 192 | if (this.blurEvent) { 193 | document.body.removeEventListener("click", this.blurEvent); 194 | document.body.removeEventListener("contextmenu", this.blurEvent); 195 | this.table.rowManager.element.removeEventListener("scroll", this.blurEvent); 196 | } 197 | }; 198 | 199 | //default accessors 200 | Menu.prototype.menus = {}; 201 | 202 | Tabulator.prototype.registerModule("menu", Menu); -------------------------------------------------------------------------------- /resources/app/lib/js/tabulator/modules/menu.min.js: -------------------------------------------------------------------------------- 1 | /* Tabulator v4.7.1 (c) Oliver Folkerd */ 2 | var Menu=function(e){this.table=e,this.menuEl=!1,this.blurEvent=this.hideMenu.bind(this),this.escEvent=this.escMenu.bind(this),this.nestedMenuBlock=!1};Menu.prototype.initializeColumnHeader=function(e){var t,n=this;e.definition.headerContextMenu&&e.getElement().addEventListener("contextmenu",function(t){var o="function"==typeof e.definition.headerContextMenu?e.definition.headerContextMenu(e.getComponent()):e.definition.headerContextMenu;t.preventDefault(),n.loadMenu(t,e,o)}),e.definition.headerMenu&&(t=document.createElement("span"),t.classList.add("tabulator-header-menu-button"),t.innerHTML="⋮",t.addEventListener("click",function(t){var o="function"==typeof e.definition.headerMenu?e.definition.headerMenu(e.getComponent()):e.definition.headerMenu;t.stopPropagation(),t.preventDefault(),n.loadMenu(t,e,o)}),e.titleElement.insertBefore(t,e.titleElement.firstChild))},Menu.prototype.initializeCell=function(e){var t=this;e.getElement().addEventListener("contextmenu",function(n){var o="function"==typeof e.column.definition.contextMenu?e.column.definition.contextMenu(e.getComponent()):e.column.definition.contextMenu;o&&n.stopImmediatePropagation(),t.loadMenu(n,e,o)})},Menu.prototype.initializeRow=function(e){var t=this;e.getElement().addEventListener("contextmenu",function(n){var o="function"==typeof t.table.options.rowContextMenu?t.table.options.rowContextMenu(e.getComponent()):t.table.options.rowContextMenu;t.loadMenu(n,e,o)})},Menu.prototype.initializeGroup=function(e){var t=this;e.getElement().addEventListener("contextmenu",function(n){var o="function"==typeof t.table.options.groupContextMenu?t.table.options.groupContextMenu(e.getComponent()):t.table.options.groupContextMenu;t.loadMenu(n,e,o)})},Menu.prototype.loadMenu=function(e,t,n){var o=this,i=Math.max(document.body.offsetHeight,window.innerHeight);if(e.preventDefault(),n&&n.length){if(this.nestedMenuBlock){if(this.isOpen())return}else this.nestedMenuBlock=setTimeout(function(){o.nestedMenuBlock=!1},100);this.hideMenu(),this.menuEl=document.createElement("div"),this.menuEl.classList.add("tabulator-menu"),n.forEach(function(e){var n=document.createElement("div"),i=e.label,u=e.disabled;e.separator?n.classList.add("tabulator-menu-separator"):(n.classList.add("tabulator-menu-item"),"function"==typeof i&&(i=i(t.getComponent())),i instanceof Node?n.appendChild(i):n.innerHTML=i,"function"==typeof u&&(u=u(t.getComponent())),u?(n.classList.add("tabulator-menu-item-disabled"),n.addEventListener("click",function(e){e.stopPropagation()})):n.addEventListener("click",function(n){o.hideMenu(),e.action(n,t.getComponent())})),o.menuEl.appendChild(n)}),this.menuEl.style.top=e.pageY+"px",this.menuEl.style.left=e.pageX+"px",document.body.addEventListener("click",this.blurEvent),this.table.rowManager.element.addEventListener("scroll",this.blurEvent),setTimeout(function(){document.body.addEventListener("contextmenu",o.blurEvent)},100),document.body.addEventListener("keydown",this.escEvent),document.body.appendChild(this.menuEl),e.pageX+this.menuEl.offsetWidth>=document.body.offsetWidth&&(this.menuEl.style.left="",this.menuEl.style.right=document.body.offsetWidth-e.pageX+"px"),e.pageY+this.menuEl.offsetHeight>=i&&(this.menuEl.style.top="",this.menuEl.style.bottom=i-e.pageY+"px")}},Menu.prototype.isOpen=function(){return!!this.menuEl.parentNode},Menu.prototype.escMenu=function(e){27==e.keyCode&&this.hideMenu()},Menu.prototype.hideMenu=function(){this.menuEl.parentNode&&this.menuEl.parentNode.removeChild(this.menuEl),this.escEvent&&document.body.removeEventListener("keydown",this.escEvent),this.blurEvent&&(document.body.removeEventListener("click",this.blurEvent),document.body.removeEventListener("contextmenu",this.blurEvent),this.table.rowManager.element.removeEventListener("scroll",this.blurEvent))},Menu.prototype.menus={},Tabulator.prototype.registerModule("menu",Menu); -------------------------------------------------------------------------------- /resources/app/lib/js/tabulator/modules/moveable_columns.min.js: -------------------------------------------------------------------------------- 1 | /* Tabulator v4.7.1 (c) Oliver Folkerd */ 2 | var MoveColumns=function(e){this.table=e,this.placeholderElement=this.createPlaceholderElement(),this.hoverElement=!1,this.checkTimeout=!1,this.checkPeriod=250,this.moving=!1,this.toCol=!1,this.toColAfter=!1,this.startX=0,this.autoScrollMargin=40,this.autoScrollStep=5,this.autoScrollTimeout=!1,this.touchMove=!1,this.moveHover=this.moveHover.bind(this),this.endMove=this.endMove.bind(this)};MoveColumns.prototype.createPlaceholderElement=function(){var e=document.createElement("div");return e.classList.add("tabulator-col"),e.classList.add("tabulator-col-placeholder"),e},MoveColumns.prototype.initializeColumn=function(e){var t,o=this,n={};e.modules.frozen||(t=e.getElement(),n.mousemove=function(n){e.parent===o.moving.parent&&((o.touchMove?n.touches[0].pageX:n.pageX)-Tabulator.prototype.helpers.elOffset(t).left+o.table.columnManager.element.scrollLeft>e.getWidth()/2?o.toCol===e&&o.toColAfter||(t.parentNode.insertBefore(o.placeholderElement,t.nextSibling),o.moveColumn(e,!0)):(o.toCol!==e||o.toColAfter)&&(t.parentNode.insertBefore(o.placeholderElement,t),o.moveColumn(e,!1)))}.bind(o),t.addEventListener("mousedown",function(t){o.touchMove=!1,1===t.which&&(o.checkTimeout=setTimeout(function(){o.startMove(t,e)},o.checkPeriod))}),t.addEventListener("mouseup",function(e){1===e.which&&o.checkTimeout&&clearTimeout(o.checkTimeout)}),o.bindTouchEvents(e)),e.modules.moveColumn=n},MoveColumns.prototype.bindTouchEvents=function(e){var t,o,n,l,i,s,m,r=this,u=e.getElement(),h=!1;u.addEventListener("touchstart",function(u){r.checkTimeout=setTimeout(function(){r.touchMove=!0,t=e,o=e.nextColumn(),l=o?o.getWidth()/2:0,n=e.prevColumn(),i=n?n.getWidth()/2:0,s=0,m=0,h=!1,r.startMove(u,e)},r.checkPeriod)},{passive:!0}),u.addEventListener("touchmove",function(u){var a,c;r.moving&&(r.moveHover(u),h||(h=u.touches[0].pageX),a=u.touches[0].pageX-h,a>0?o&&a-s>l&&(c=o)!==e&&(h=u.touches[0].pageX,c.getElement().parentNode.insertBefore(r.placeholderElement,c.getElement().nextSibling),r.moveColumn(c,!0)):n&&-a-m>i&&(c=n)!==e&&(h=u.touches[0].pageX,c.getElement().parentNode.insertBefore(r.placeholderElement,c.getElement()),r.moveColumn(c,!1)),c&&(t=c,o=c.nextColumn(),s=l,l=o?o.getWidth()/2:0,n=c.prevColumn(),m=i,i=n?n.getWidth()/2:0))},{passive:!0}),u.addEventListener("touchend",function(e){r.checkTimeout&&clearTimeout(r.checkTimeout),r.moving&&r.endMove(e)})},MoveColumns.prototype.startMove=function(e,t){var o=t.getElement();this.moving=t,this.startX=(this.touchMove?e.touches[0].pageX:e.pageX)-Tabulator.prototype.helpers.elOffset(o).left,this.table.element.classList.add("tabulator-block-select"),this.placeholderElement.style.width=t.getWidth()+"px",this.placeholderElement.style.height=t.getHeight()+"px",o.parentNode.insertBefore(this.placeholderElement,o),o.parentNode.removeChild(o),this.hoverElement=o.cloneNode(!0),this.hoverElement.classList.add("tabulator-moving"),this.table.columnManager.getElement().appendChild(this.hoverElement),this.hoverElement.style.left="0",this.hoverElement.style.bottom="0",this.touchMove||(this._bindMouseMove(),document.body.addEventListener("mousemove",this.moveHover),document.body.addEventListener("mouseup",this.endMove)),this.moveHover(e)},MoveColumns.prototype._bindMouseMove=function(){this.table.columnManager.columnsByIndex.forEach(function(e){e.modules.moveColumn.mousemove&&e.getElement().addEventListener("mousemove",e.modules.moveColumn.mousemove)})},MoveColumns.prototype._unbindMouseMove=function(){this.table.columnManager.columnsByIndex.forEach(function(e){e.modules.moveColumn.mousemove&&e.getElement().removeEventListener("mousemove",e.modules.moveColumn.mousemove)})},MoveColumns.prototype.moveColumn=function(e,t){var o=this.moving.getCells();this.toCol=e,this.toColAfter=t,t?e.getCells().forEach(function(e,t){var n=e.getElement();n.parentNode.insertBefore(o[t].getElement(),n.nextSibling)}):e.getCells().forEach(function(e,t){var n=e.getElement();n.parentNode.insertBefore(o[t].getElement(),n)})},MoveColumns.prototype.endMove=function(e){(1===e.which||this.touchMove)&&(this._unbindMouseMove(),this.placeholderElement.parentNode.insertBefore(this.moving.getElement(),this.placeholderElement.nextSibling),this.placeholderElement.parentNode.removeChild(this.placeholderElement),this.hoverElement.parentNode.removeChild(this.hoverElement),this.table.element.classList.remove("tabulator-block-select"),this.toCol&&this.table.columnManager.moveColumnActual(this.moving,this.toCol,this.toColAfter),this.moving=!1,this.toCol=!1,this.toColAfter=!1,this.touchMove||(document.body.removeEventListener("mousemove",this.moveHover),document.body.removeEventListener("mouseup",this.endMove)))},MoveColumns.prototype.moveHover=function(e){var t,o=this,n=o.table.columnManager.getElement(),l=n.scrollLeft,i=(o.touchMove?e.touches[0].pageX:e.pageX)-Tabulator.prototype.helpers.elOffset(n).left+l;o.hoverElement.style.left=i-o.startX+"px",i-lo?s.splice(o,0,e):s.push(e))}),s},Persistence.prototype._findColumn=function(e,t){var i=t.columns?"group":t.field?"field":"object";return e.find(function(e){switch(i){case"group":return e.title===t.title&&e.columns.length===t.columns.length;case"field":return e.field===t.field;case"object":return e===t}})},Persistence.prototype.save=function(e){var t={};switch(e){case"columns":t=this.parseColumns(this.table.columnManager.getColumns());break;case"filter":t=this.table.modules.filter.getFilters();break;case"sort":t=this.validateSorters(this.table.modules.sort.getSort());break;case"group":t=this.getGroupConfig();break;case"page":t=this.getPageConfig()}this.writeFunc&&this.writeFunc(this.id,e,t)},Persistence.prototype.validateSorters=function(e){return e.forEach(function(e){e.column=e.field,delete e.field}),e},Persistence.prototype.getGroupConfig=function(){return this.config.group&&((!0===this.config.group||this.config.group.groupBy)&&(data.groupBy=this.table.options.groupBy),(!0===this.config.group||this.config.group.groupStartOpen)&&(data.groupStartOpen=this.table.options.groupStartOpen),(!0===this.config.group||this.config.group.groupHeader)&&(data.groupHeader=this.table.options.groupHeader)),data},Persistence.prototype.getPageConfig=function(){var e={};return this.config.page&&((!0===this.config.page||this.config.page.size)&&(e.paginationSize=this.table.modules.page.getPageSize()),(!0===this.config.page||this.config.page.page)&&(e.paginationInitialPage=this.table.modules.page.getPage())),e},Persistence.prototype.parseColumns=function(e){var t=this,i=[];return e.forEach(function(e){var s,o={},n=e.getDefinition();e.isGroup?(o.title=n.title,o.columns=t.parseColumns(e.getColumns())):(o.field=e.getField(),!0===t.config.columns||void 0==t.config.columns?(s=Object.keys(n),s.push("width")):s=t.config.columns,s.forEach(function(t){switch(t){case"width":o.width=e.getWidth();break;case"visible":o.visible=e.visible;break;default:o[t]=n[t]}})),i.push(o)}),i},Persistence.prototype.readers={local:function(e,t){var i=localStorage.getItem(e+"-"+t);return!!i&&JSON.parse(i)},cookie:function(e,t){var i,s,o=document.cookie,n=e+"-"+t,r=o.indexOf(n+"=");return r>-1&&(o=o.substr(r),i=o.indexOf(";"),i>-1&&(o=o.substr(0,i)),s=o.replace(n+"=","")),!!s&&JSON.parse(s)}},Persistence.prototype.writers={local:function(e,t,i){localStorage.setItem(e+"-"+t,JSON.stringify(i))},cookie:function(e,t,i){var s=new Date;s.setDate(s.getDate()+1e4),document.cookie=e+"-"+t+"="+JSON.stringify(i)+"; expires="+s.toUTCString()}},Tabulator.prototype.registerModule("persistence",Persistence); -------------------------------------------------------------------------------- /resources/app/lib/js/tabulator/modules/print.js: -------------------------------------------------------------------------------- 1 | /* Tabulator v4.7.1 (c) Oliver Folkerd */ 2 | 3 | var Print = function Print(table) { 4 | this.table = table; //hold Tabulator object 5 | this.element = false; 6 | this.manualBlock = false; 7 | }; 8 | 9 | Print.prototype.initialize = function () { 10 | window.addEventListener("beforeprint", this.replaceTable.bind(this)); 11 | window.addEventListener("afterprint", this.cleanup.bind(this)); 12 | }; 13 | 14 | Print.prototype.replaceTable = function () { 15 | if (!this.manualBlock) { 16 | this.element = document.createElement("div"); 17 | this.element.classList.add("tabulator-print-table"); 18 | 19 | this.element.appendChild(this.table.modules.export.genereateTable(this.table.options.printConfig, this.table.options.printStyled, this.table.options.printRowRange, "print")); 20 | 21 | this.table.element.style.display = "none"; 22 | 23 | this.table.element.parentNode.insertBefore(this.element, this.table.element); 24 | } 25 | }; 26 | 27 | Print.prototype.cleanup = function () { 28 | document.body.classList.remove("tabulator-print-fullscreen-hide"); 29 | 30 | if (this.element && this.element.parentNode) { 31 | this.element.parentNode.removeChild(this.element); 32 | this.table.element.style.display = ""; 33 | } 34 | }; 35 | 36 | Print.prototype.printFullscreen = function (visible, style, config) { 37 | var scrollX = window.scrollX, 38 | scrollY = window.scrollY, 39 | headerEl = document.createElement("div"), 40 | footerEl = document.createElement("div"), 41 | tableEl = this.table.modules.export.genereateTable(typeof config != "undefined" ? config : this.table.options.printConfig, typeof style != "undefined" ? style : this.table.options.printStyled, visible, "print"), 42 | headerContent, 43 | footerContent; 44 | 45 | this.manualBlock = true; 46 | 47 | this.element = document.createElement("div"); 48 | this.element.classList.add("tabulator-print-fullscreen"); 49 | 50 | if (this.table.options.printHeader) { 51 | headerEl.classList.add("tabulator-print-header"); 52 | 53 | headerContent = typeof this.table.options.printHeader == "function" ? this.table.options.printHeader.call(this.table) : this.table.options.printHeader; 54 | 55 | if (typeof headerContent == "string") { 56 | headerEl.innerHTML = headerContent; 57 | } else { 58 | headerEl.appendChild(headerContent); 59 | } 60 | 61 | this.element.appendChild(headerEl); 62 | } 63 | 64 | this.element.appendChild(tableEl); 65 | 66 | if (this.table.options.printFooter) { 67 | footerEl.classList.add("tabulator-print-footer"); 68 | 69 | footerContent = typeof this.table.options.printFooter == "function" ? this.table.options.printFooter.call(this.table) : this.table.options.printFooter; 70 | 71 | if (typeof footerContent == "string") { 72 | footerEl.innerHTML = footerContent; 73 | } else { 74 | footerEl.appendChild(footerContent); 75 | } 76 | 77 | this.element.appendChild(footerEl); 78 | } 79 | 80 | document.body.classList.add("tabulator-print-fullscreen-hide"); 81 | document.body.appendChild(this.element); 82 | 83 | if (this.table.options.printFormatter) { 84 | this.table.options.printFormatter(this.element, tableEl); 85 | } 86 | 87 | window.print(); 88 | 89 | this.cleanup(); 90 | 91 | window.scrollTo(scrollX, scrollY); 92 | 93 | this.manualBlock = false; 94 | }; 95 | 96 | Tabulator.prototype.registerModule("print", Print); -------------------------------------------------------------------------------- /resources/app/lib/js/tabulator/modules/print.min.js: -------------------------------------------------------------------------------- 1 | /* Tabulator v4.7.1 (c) Oliver Folkerd */ 2 | var Print=function(t){this.table=t,this.element=!1,this.manualBlock=!1};Print.prototype.initialize=function(){window.addEventListener("beforeprint",this.replaceTable.bind(this)),window.addEventListener("afterprint",this.cleanup.bind(this))},Print.prototype.replaceTable=function(){this.manualBlock||(this.element=document.createElement("div"),this.element.classList.add("tabulator-print-table"),this.element.appendChild(this.table.modules.export.genereateTable(this.table.options.printConfig,this.table.options.printStyled,this.table.options.printRowRange,"print")),this.table.element.style.display="none",this.table.element.parentNode.insertBefore(this.element,this.table.element))},Print.prototype.cleanup=function(){document.body.classList.remove("tabulator-print-fullscreen-hide"),this.element&&this.element.parentNode&&(this.element.parentNode.removeChild(this.element),this.table.element.style.display="")},Print.prototype.printFullscreen=function(t,e,i){var n,l,o=window.scrollX,a=window.scrollY,s=document.createElement("div"),r=document.createElement("div"),p=this.table.modules.export.genereateTable(void 0!==i?i:this.table.options.printConfig,void 0!==e?e:this.table.options.printStyled,t,"print");this.manualBlock=!0,this.element=document.createElement("div"),this.element.classList.add("tabulator-print-fullscreen"),this.table.options.printHeader&&(s.classList.add("tabulator-print-header"),n="function"==typeof this.table.options.printHeader?this.table.options.printHeader.call(this.table):this.table.options.printHeader,"string"==typeof n?s.innerHTML=n:s.appendChild(n),this.element.appendChild(s)),this.element.appendChild(p),this.table.options.printFooter&&(r.classList.add("tabulator-print-footer"),l="function"==typeof this.table.options.printFooter?this.table.options.printFooter.call(this.table):this.table.options.printFooter,"string"==typeof l?r.innerHTML=l:r.appendChild(l),this.element.appendChild(r)),document.body.classList.add("tabulator-print-fullscreen-hide"),document.body.appendChild(this.element),this.table.options.printFormatter&&this.table.options.printFormatter(this.element,p),window.print(),this.cleanup(),window.scrollTo(o,a),this.manualBlock=!1},Tabulator.prototype.registerModule("print",Print); -------------------------------------------------------------------------------- /resources/app/lib/js/tabulator/modules/reactive_data.js: -------------------------------------------------------------------------------- 1 | /* Tabulator v4.7.1 (c) Oliver Folkerd */ 2 | 3 | var ReactiveData = function ReactiveData(table) { 4 | this.table = table; //hold Tabulator object 5 | this.data = false; 6 | this.blocked = false; //block reactivity while performing update 7 | this.origFuncs = {}; // hold original data array functions to allow replacement after data is done with 8 | this.currentVersion = 0; 9 | }; 10 | 11 | ReactiveData.prototype.watchData = function (data) { 12 | var self = this, 13 | pushFunc, 14 | version; 15 | 16 | this.currentVersion++; 17 | 18 | version = this.currentVersion; 19 | 20 | self.unwatchData(); 21 | 22 | self.data = data; 23 | 24 | //override array push function 25 | self.origFuncs.push = data.push; 26 | 27 | Object.defineProperty(self.data, "push", { 28 | enumerable: false, 29 | configurable: true, 30 | value: function value() { 31 | var args = Array.from(arguments); 32 | 33 | if (!self.blocked && version === self.currentVersion) { 34 | args.forEach(function (arg) { 35 | self.table.rowManager.addRowActual(arg, false); 36 | }); 37 | } 38 | 39 | return self.origFuncs.push.apply(data, arguments); 40 | } 41 | }); 42 | 43 | //override array unshift function 44 | self.origFuncs.unshift = data.unshift; 45 | 46 | Object.defineProperty(self.data, "unshift", { 47 | enumerable: false, 48 | configurable: true, 49 | value: function value() { 50 | var args = Array.from(arguments); 51 | 52 | if (!self.blocked && version === self.currentVersion) { 53 | args.forEach(function (arg) { 54 | self.table.rowManager.addRowActual(arg, true); 55 | }); 56 | } 57 | 58 | return self.origFuncs.unshift.apply(data, arguments); 59 | } 60 | }); 61 | 62 | //override array shift function 63 | self.origFuncs.shift = data.shift; 64 | 65 | Object.defineProperty(self.data, "shift", { 66 | enumerable: false, 67 | configurable: true, 68 | value: function value() { 69 | var row; 70 | 71 | if (!self.blocked && version === self.currentVersion) { 72 | if (self.data.length) { 73 | row = self.table.rowManager.getRowFromDataObject(self.data[0]); 74 | 75 | if (row) { 76 | row.deleteActual(); 77 | } 78 | } 79 | } 80 | 81 | return self.origFuncs.shift.call(data); 82 | } 83 | }); 84 | 85 | //override array pop function 86 | self.origFuncs.pop = data.pop; 87 | 88 | Object.defineProperty(self.data, "pop", { 89 | enumerable: false, 90 | configurable: true, 91 | value: function value() { 92 | var row; 93 | if (!self.blocked && version === self.currentVersion) { 94 | if (self.data.length) { 95 | row = self.table.rowManager.getRowFromDataObject(self.data[self.data.length - 1]); 96 | 97 | if (row) { 98 | row.deleteActual(); 99 | } 100 | } 101 | } 102 | return self.origFuncs.pop.call(data); 103 | } 104 | }); 105 | 106 | //override array splice function 107 | self.origFuncs.splice = data.splice; 108 | 109 | Object.defineProperty(self.data, "splice", { 110 | enumerable: false, 111 | configurable: true, 112 | value: function value() { 113 | var args = Array.from(arguments), 114 | start = args[0] < 0 ? data.length + args[0] : args[0], 115 | end = args[1], 116 | newRows = args[2] ? args.slice(2) : false, 117 | startRow; 118 | 119 | if (!self.blocked && version === self.currentVersion) { 120 | 121 | //add new rows 122 | if (newRows) { 123 | startRow = data[start] ? self.table.rowManager.getRowFromDataObject(data[start]) : false; 124 | 125 | if (startRow) { 126 | newRows.forEach(function (rowData) { 127 | self.table.rowManager.addRowActual(rowData, true, startRow, true); 128 | }); 129 | } else { 130 | newRows = newRows.slice().reverse(); 131 | 132 | newRows.forEach(function (rowData) { 133 | self.table.rowManager.addRowActual(rowData, true, false, true); 134 | }); 135 | } 136 | } 137 | 138 | //delete removed rows 139 | if (end !== 0) { 140 | var oldRows = data.slice(start, typeof args[1] === "undefined" ? args[1] : start + end); 141 | 142 | oldRows.forEach(function (rowData, i) { 143 | var row = self.table.rowManager.getRowFromDataObject(rowData); 144 | 145 | if (row) { 146 | row.deleteActual(i !== oldRows.length - 1); 147 | } 148 | }); 149 | } 150 | 151 | if (newRows || end !== 0) { 152 | self.table.rowManager.reRenderInPosition(); 153 | } 154 | } 155 | 156 | return self.origFuncs.splice.apply(data, arguments); 157 | } 158 | }); 159 | }; 160 | 161 | ReactiveData.prototype.unwatchData = function () { 162 | if (this.data !== false) { 163 | for (var key in this.origFuncs) { 164 | Object.defineProperty(this.data, key, { 165 | enumerable: true, 166 | configurable: true, 167 | writable: true, 168 | value: this.origFuncs.key 169 | }); 170 | } 171 | } 172 | }; 173 | 174 | ReactiveData.prototype.watchRow = function (row) { 175 | var self = this, 176 | data = row.getData(); 177 | 178 | this.blocked = true; 179 | 180 | for (var key in data) { 181 | this.watchKey(row, data, key); 182 | } 183 | 184 | this.blocked = false; 185 | }; 186 | 187 | ReactiveData.prototype.watchKey = function (row, data, key) { 188 | var self = this, 189 | props = Object.getOwnPropertyDescriptor(data, key), 190 | value = data[key], 191 | version = this.currentVersion; 192 | 193 | Object.defineProperty(data, key, { 194 | set: function set(newValue) { 195 | value = newValue; 196 | if (!self.blocked && version === self.currentVersion) { 197 | var update = {}; 198 | update[key] = newValue; 199 | row.updateData(update); 200 | } 201 | 202 | if (props.set) { 203 | props.set(newValue); 204 | } 205 | }, 206 | get: function get() { 207 | 208 | if (props.get) { 209 | props.get(); 210 | } 211 | 212 | return value; 213 | } 214 | }); 215 | }; 216 | 217 | ReactiveData.prototype.unwatchRow = function (row) { 218 | var data = row.getData(); 219 | 220 | for (var key in data) { 221 | Object.defineProperty(data, key, { 222 | value: data[key] 223 | }); 224 | } 225 | }; 226 | 227 | ReactiveData.prototype.block = function () { 228 | this.blocked = true; 229 | }; 230 | 231 | ReactiveData.prototype.unblock = function () { 232 | this.blocked = false; 233 | }; 234 | 235 | Tabulator.prototype.registerModule("reactiveData", ReactiveData); -------------------------------------------------------------------------------- /resources/app/lib/js/tabulator/modules/reactive_data.min.js: -------------------------------------------------------------------------------- 1 | /* Tabulator v4.7.1 (c) Oliver Folkerd */ 2 | var ReactiveData=function(e){this.table=e,this.data=!1,this.blocked=!1,this.origFuncs={},this.currentVersion=0};ReactiveData.prototype.watchData=function(e){var t,a=this;this.currentVersion++,t=this.currentVersion,a.unwatchData(),a.data=e,a.origFuncs.push=e.push,Object.defineProperty(a.data,"push",{enumerable:!1,configurable:!0,value:function(){var r=Array.from(arguments);return a.blocked||t!==a.currentVersion||r.forEach(function(e){a.table.rowManager.addRowActual(e,!1)}),a.origFuncs.push.apply(e,arguments)}}),a.origFuncs.unshift=e.unshift,Object.defineProperty(a.data,"unshift",{enumerable:!1,configurable:!0,value:function(){var r=Array.from(arguments);return a.blocked||t!==a.currentVersion||r.forEach(function(e){a.table.rowManager.addRowActual(e,!0)}),a.origFuncs.unshift.apply(e,arguments)}}),a.origFuncs.shift=e.shift,Object.defineProperty(a.data,"shift",{enumerable:!1,configurable:!0,value:function(){var r;return a.blocked||t!==a.currentVersion||a.data.length&&(r=a.table.rowManager.getRowFromDataObject(a.data[0]))&&r.deleteActual(),a.origFuncs.shift.call(e)}}),a.origFuncs.pop=e.pop,Object.defineProperty(a.data,"pop",{enumerable:!1,configurable:!0,value:function(){var r;return a.blocked||t!==a.currentVersion||a.data.length&&(r=a.table.rowManager.getRowFromDataObject(a.data[a.data.length-1]))&&r.deleteActual(),a.origFuncs.pop.call(e)}}),a.origFuncs.splice=e.splice,Object.defineProperty(a.data,"splice",{enumerable:!1,configurable:!0,value:function(){var r,o=Array.from(arguments),n=o[0]<0?e.length+o[0]:o[0],c=o[1],i=!!o[2]&&o.slice(2);if(!a.blocked&&t===a.currentVersion){if(i&&(r=!!e[n]&&a.table.rowManager.getRowFromDataObject(e[n]),r?i.forEach(function(e){a.table.rowManager.addRowActual(e,!0,r,!0)}):(i=i.slice().reverse(),i.forEach(function(e){a.table.rowManager.addRowActual(e,!0,!1,!0)}))),0!==c){var u=e.slice(n,void 0===o[1]?o[1]:n+c);u.forEach(function(e,t){var r=a.table.rowManager.getRowFromDataObject(e);r&&r.deleteActual(t!==u.length-1)})}(i||0!==c)&&a.table.rowManager.reRenderInPosition()}return a.origFuncs.splice.apply(e,arguments)}})},ReactiveData.prototype.unwatchData=function(){if(!1!==this.data)for(var e in this.origFuncs)Object.defineProperty(this.data,e,{enumerable:!0,configurable:!0,writable:!0,value:this.origFuncs.key})},ReactiveData.prototype.watchRow=function(e){var t=e.getData();this.blocked=!0;for(var a in t)this.watchKey(e,t,a);this.blocked=!1},ReactiveData.prototype.watchKey=function(e,t,a){var r=this,o=Object.getOwnPropertyDescriptor(t,a),n=t[a],c=this.currentVersion;Object.defineProperty(t,a,{set:function(t){if(n=t,!r.blocked&&c===r.currentVersion){var i={};i[a]=t,e.updateData(i)}o.set&&o.set(t)},get:function(){return o.get&&o.get(),n}})},ReactiveData.prototype.unwatchRow=function(e){var t=e.getData();for(var a in t)Object.defineProperty(t,a,{value:t[a]})},ReactiveData.prototype.block=function(){this.blocked=!0},ReactiveData.prototype.unblock=function(){this.blocked=!1},Tabulator.prototype.registerModule("reactiveData",ReactiveData); -------------------------------------------------------------------------------- /resources/app/lib/js/tabulator/modules/resize_columns.js: -------------------------------------------------------------------------------- 1 | /* Tabulator v4.7.1 (c) Oliver Folkerd */ 2 | 3 | var ResizeColumns = function ResizeColumns(table) { 4 | this.table = table; //hold Tabulator object 5 | this.startColumn = false; 6 | this.startX = false; 7 | this.startWidth = false; 8 | this.handle = null; 9 | this.prevHandle = null; 10 | }; 11 | 12 | ResizeColumns.prototype.initializeColumn = function (type, column, element) { 13 | var self = this, 14 | variableHeight = false, 15 | mode = this.table.options.resizableColumns; 16 | 17 | //set column resize mode 18 | if (type === "header") { 19 | variableHeight = column.definition.formatter == "textarea" || column.definition.variableHeight; 20 | column.modules.resize = { variableHeight: variableHeight }; 21 | } 22 | 23 | if (mode === true || mode == type) { 24 | 25 | var handle = document.createElement('div'); 26 | handle.className = "tabulator-col-resize-handle"; 27 | 28 | var prevHandle = document.createElement('div'); 29 | prevHandle.className = "tabulator-col-resize-handle prev"; 30 | 31 | handle.addEventListener("click", function (e) { 32 | e.stopPropagation(); 33 | }); 34 | 35 | var handleDown = function handleDown(e) { 36 | var nearestColumn = column.getLastColumn(); 37 | 38 | if (nearestColumn && self._checkResizability(nearestColumn)) { 39 | self.startColumn = column; 40 | self._mouseDown(e, nearestColumn, handle); 41 | } 42 | }; 43 | 44 | handle.addEventListener("mousedown", handleDown); 45 | handle.addEventListener("touchstart", handleDown, { passive: true }); 46 | 47 | //reszie column on double click 48 | handle.addEventListener("dblclick", function (e) { 49 | var col = column.getLastColumn(); 50 | 51 | if (col && self._checkResizability(col)) { 52 | e.stopPropagation(); 53 | col.reinitializeWidth(true); 54 | } 55 | }); 56 | 57 | prevHandle.addEventListener("click", function (e) { 58 | e.stopPropagation(); 59 | }); 60 | 61 | var prevHandleDown = function prevHandleDown(e) { 62 | var nearestColumn, colIndex, prevColumn; 63 | 64 | nearestColumn = column.getFirstColumn(); 65 | 66 | if (nearestColumn) { 67 | colIndex = self.table.columnManager.findColumnIndex(nearestColumn); 68 | prevColumn = colIndex > 0 ? self.table.columnManager.getColumnByIndex(colIndex - 1) : false; 69 | 70 | if (prevColumn && self._checkResizability(prevColumn)) { 71 | self.startColumn = column; 72 | self._mouseDown(e, prevColumn, prevHandle); 73 | } 74 | } 75 | }; 76 | 77 | prevHandle.addEventListener("mousedown", prevHandleDown); 78 | prevHandle.addEventListener("touchstart", prevHandleDown, { passive: true }); 79 | 80 | //resize column on double click 81 | prevHandle.addEventListener("dblclick", function (e) { 82 | var nearestColumn, colIndex, prevColumn; 83 | 84 | nearestColumn = column.getFirstColumn(); 85 | 86 | if (nearestColumn) { 87 | colIndex = self.table.columnManager.findColumnIndex(nearestColumn); 88 | prevColumn = colIndex > 0 ? self.table.columnManager.getColumnByIndex(colIndex - 1) : false; 89 | 90 | if (prevColumn && self._checkResizability(prevColumn)) { 91 | e.stopPropagation(); 92 | prevColumn.reinitializeWidth(true); 93 | } 94 | } 95 | }); 96 | 97 | element.appendChild(handle); 98 | element.appendChild(prevHandle); 99 | } 100 | }; 101 | 102 | ResizeColumns.prototype._checkResizability = function (column) { 103 | return typeof column.definition.resizable != "undefined" ? column.definition.resizable : this.table.options.resizableColumns; 104 | }; 105 | 106 | ResizeColumns.prototype._mouseDown = function (e, column, handle) { 107 | var self = this; 108 | 109 | self.table.element.classList.add("tabulator-block-select"); 110 | 111 | function mouseMove(e) { 112 | // self.table.columnManager.tempScrollBlock(); 113 | 114 | column.setWidth(self.startWidth + ((typeof e.screenX === "undefined" ? e.touches[0].screenX : e.screenX) - self.startX)); 115 | 116 | if (!self.table.browserSlow && column.modules.resize && column.modules.resize.variableHeight) { 117 | column.checkCellHeights(); 118 | } 119 | } 120 | 121 | function mouseUp(e) { 122 | 123 | //block editor from taking action while resizing is taking place 124 | if (self.startColumn.modules.edit) { 125 | self.startColumn.modules.edit.blocked = false; 126 | } 127 | 128 | if (self.table.browserSlow && column.modules.resize && column.modules.resize.variableHeight) { 129 | column.checkCellHeights(); 130 | } 131 | 132 | document.body.removeEventListener("mouseup", mouseUp); 133 | document.body.removeEventListener("mousemove", mouseMove); 134 | 135 | handle.removeEventListener("touchmove", mouseMove); 136 | handle.removeEventListener("touchend", mouseUp); 137 | 138 | self.table.element.classList.remove("tabulator-block-select"); 139 | 140 | if (self.table.options.persistence && self.table.modExists("persistence", true) && self.table.modules.persistence.config.columns) { 141 | self.table.modules.persistence.save("columns"); 142 | } 143 | 144 | self.table.options.columnResized.call(self.table, column.getComponent()); 145 | } 146 | 147 | e.stopPropagation(); //prevent resize from interfereing with movable columns 148 | 149 | //block editor from taking action while resizing is taking place 150 | if (self.startColumn.modules.edit) { 151 | self.startColumn.modules.edit.blocked = true; 152 | } 153 | 154 | self.startX = typeof e.screenX === "undefined" ? e.touches[0].screenX : e.screenX; 155 | self.startWidth = column.getWidth(); 156 | 157 | document.body.addEventListener("mousemove", mouseMove); 158 | document.body.addEventListener("mouseup", mouseUp); 159 | handle.addEventListener("touchmove", mouseMove, { passive: true }); 160 | handle.addEventListener("touchend", mouseUp); 161 | }; 162 | 163 | Tabulator.prototype.registerModule("resizeColumns", ResizeColumns); -------------------------------------------------------------------------------- /resources/app/lib/js/tabulator/modules/resize_columns.min.js: -------------------------------------------------------------------------------- 1 | /* Tabulator v4.7.1 (c) Oliver Folkerd */ 2 | var ResizeColumns=function(e){this.table=e,this.startColumn=!1,this.startX=!1,this.startWidth=!1,this.handle=null,this.prevHandle=null};ResizeColumns.prototype.initializeColumn=function(e,t,n){var o=this,i=!1,s=this.table.options.resizableColumns;if("header"===e&&(i="textarea"==t.definition.formatter||t.definition.variableHeight,t.modules.resize={variableHeight:i}),!0===s||s==e){var a=document.createElement("div");a.className="tabulator-col-resize-handle";var l=document.createElement("div");l.className="tabulator-col-resize-handle prev",a.addEventListener("click",function(e){e.stopPropagation()});var r=function(e){var n=t.getLastColumn();n&&o._checkResizability(n)&&(o.startColumn=t,o._mouseDown(e,n,a))};a.addEventListener("mousedown",r),a.addEventListener("touchstart",r,{passive:!0}),a.addEventListener("dblclick",function(e){var n=t.getLastColumn();n&&o._checkResizability(n)&&(e.stopPropagation(),n.reinitializeWidth(!0))}),l.addEventListener("click",function(e){e.stopPropagation()});var d=function(e){var n,i,s;(n=t.getFirstColumn())&&(i=o.table.columnManager.findColumnIndex(n),(s=i>0&&o.table.columnManager.getColumnByIndex(i-1))&&o._checkResizability(s)&&(o.startColumn=t,o._mouseDown(e,s,l)))};l.addEventListener("mousedown",d),l.addEventListener("touchstart",d,{passive:!0}),l.addEventListener("dblclick",function(e){var n,i,s;(n=t.getFirstColumn())&&(i=o.table.columnManager.findColumnIndex(n),(s=i>0&&o.table.columnManager.getColumnByIndex(i-1))&&o._checkResizability(s)&&(e.stopPropagation(),s.reinitializeWidth(!0)))}),n.appendChild(a),n.appendChild(l)}},ResizeColumns.prototype._checkResizability=function(e){return void 0!==e.definition.resizable?e.definition.resizable:this.table.options.resizableColumns},ResizeColumns.prototype._mouseDown=function(e,t,n){function o(e){t.setWidth(s.startWidth+((void 0===e.screenX?e.touches[0].screenX:e.screenX)-s.startX)),!s.table.browserSlow&&t.modules.resize&&t.modules.resize.variableHeight&&t.checkCellHeights()}function i(e){s.startColumn.modules.edit&&(s.startColumn.modules.edit.blocked=!1),s.table.browserSlow&&t.modules.resize&&t.modules.resize.variableHeight&&t.checkCellHeights(),document.body.removeEventListener("mouseup",i),document.body.removeEventListener("mousemove",o),n.removeEventListener("touchmove",o),n.removeEventListener("touchend",i),s.table.element.classList.remove("tabulator-block-select"),s.table.options.persistence&&s.table.modExists("persistence",!0)&&s.table.modules.persistence.config.columns&&s.table.modules.persistence.save("columns"),s.table.options.columnResized.call(s.table,t.getComponent())}var s=this;s.table.element.classList.add("tabulator-block-select"),e.stopPropagation(),s.startColumn.modules.edit&&(s.startColumn.modules.edit.blocked=!0),s.startX=void 0===e.screenX?e.touches[0].screenX:e.screenX,s.startWidth=t.getWidth(),document.body.addEventListener("mousemove",o),document.body.addEventListener("mouseup",i),n.addEventListener("touchmove",o,{passive:!0}),n.addEventListener("touchend",i)},Tabulator.prototype.registerModule("resizeColumns",ResizeColumns); -------------------------------------------------------------------------------- /resources/app/lib/js/tabulator/modules/resize_rows.js: -------------------------------------------------------------------------------- 1 | /* Tabulator v4.7.1 (c) Oliver Folkerd */ 2 | 3 | var ResizeRows = function ResizeRows(table) { 4 | this.table = table; //hold Tabulator object 5 | this.startColumn = false; 6 | this.startY = false; 7 | this.startHeight = false; 8 | this.handle = null; 9 | this.prevHandle = null; 10 | }; 11 | 12 | ResizeRows.prototype.initializeRow = function (row) { 13 | var self = this, 14 | rowEl = row.getElement(); 15 | 16 | var handle = document.createElement('div'); 17 | handle.className = "tabulator-row-resize-handle"; 18 | 19 | var prevHandle = document.createElement('div'); 20 | prevHandle.className = "tabulator-row-resize-handle prev"; 21 | 22 | handle.addEventListener("click", function (e) { 23 | e.stopPropagation(); 24 | }); 25 | 26 | var handleDown = function handleDown(e) { 27 | self.startRow = row; 28 | self._mouseDown(e, row, handle); 29 | }; 30 | 31 | handle.addEventListener("mousedown", handleDown); 32 | handle.addEventListener("touchstart", handleDown, { passive: true }); 33 | 34 | prevHandle.addEventListener("click", function (e) { 35 | e.stopPropagation(); 36 | }); 37 | 38 | var prevHandleDown = function prevHandleDown(e) { 39 | var prevRow = self.table.rowManager.prevDisplayRow(row); 40 | 41 | if (prevRow) { 42 | self.startRow = prevRow; 43 | self._mouseDown(e, prevRow, prevHandle); 44 | } 45 | }; 46 | 47 | prevHandle.addEventListener("mousedown", prevHandleDown); 48 | prevHandle.addEventListener("touchstart", prevHandleDown, { passive: true }); 49 | 50 | rowEl.appendChild(handle); 51 | rowEl.appendChild(prevHandle); 52 | }; 53 | 54 | ResizeRows.prototype._mouseDown = function (e, row, handle) { 55 | var self = this; 56 | 57 | self.table.element.classList.add("tabulator-block-select"); 58 | 59 | function mouseMove(e) { 60 | row.setHeight(self.startHeight + ((typeof e.screenY === "undefined" ? e.touches[0].screenY : e.screenY) - self.startY)); 61 | } 62 | 63 | function mouseUp(e) { 64 | 65 | // //block editor from taking action while resizing is taking place 66 | // if(self.startColumn.modules.edit){ 67 | // self.startColumn.modules.edit.blocked = false; 68 | // } 69 | 70 | document.body.removeEventListener("mouseup", mouseMove); 71 | document.body.removeEventListener("mousemove", mouseMove); 72 | 73 | handle.removeEventListener("touchmove", mouseMove); 74 | handle.removeEventListener("touchend", mouseUp); 75 | 76 | self.table.element.classList.remove("tabulator-block-select"); 77 | 78 | self.table.options.rowResized.call(this.table, row.getComponent()); 79 | } 80 | 81 | e.stopPropagation(); //prevent resize from interfereing with movable columns 82 | 83 | //block editor from taking action while resizing is taking place 84 | // if(self.startColumn.modules.edit){ 85 | // self.startColumn.modules.edit.blocked = true; 86 | // } 87 | 88 | self.startY = typeof e.screenY === "undefined" ? e.touches[0].screenY : e.screenY; 89 | self.startHeight = row.getHeight(); 90 | 91 | document.body.addEventListener("mousemove", mouseMove); 92 | document.body.addEventListener("mouseup", mouseUp); 93 | 94 | handle.addEventListener("touchmove", mouseMove, { passive: true }); 95 | handle.addEventListener("touchend", mouseUp); 96 | }; 97 | 98 | Tabulator.prototype.registerModule("resizeRows", ResizeRows); -------------------------------------------------------------------------------- /resources/app/lib/js/tabulator/modules/resize_rows.min.js: -------------------------------------------------------------------------------- 1 | /* Tabulator v4.7.1 (c) Oliver Folkerd */ 2 | var ResizeRows=function(e){this.table=e,this.startColumn=!1,this.startY=!1,this.startHeight=!1,this.handle=null,this.prevHandle=null};ResizeRows.prototype.initializeRow=function(e){var t=this,o=e.getElement(),s=document.createElement("div");s.className="tabulator-row-resize-handle";var n=document.createElement("div");n.className="tabulator-row-resize-handle prev",s.addEventListener("click",function(e){e.stopPropagation()});var a=function(o){t.startRow=e,t._mouseDown(o,e,s)};s.addEventListener("mousedown",a),s.addEventListener("touchstart",a,{passive:!0}),n.addEventListener("click",function(e){e.stopPropagation()});var r=function(o){var s=t.table.rowManager.prevDisplayRow(e);s&&(t.startRow=s,t._mouseDown(o,s,n))};n.addEventListener("mousedown",r),n.addEventListener("touchstart",r,{passive:!0}),o.appendChild(s),o.appendChild(n)},ResizeRows.prototype._mouseDown=function(e,t,o){function s(e){t.setHeight(a.startHeight+((void 0===e.screenY?e.touches[0].screenY:e.screenY)-a.startY))}function n(e){document.body.removeEventListener("mouseup",s),document.body.removeEventListener("mousemove",s),o.removeEventListener("touchmove",s),o.removeEventListener("touchend",n),a.table.element.classList.remove("tabulator-block-select"),a.table.options.rowResized.call(this.table,t.getComponent())}var a=this;a.table.element.classList.add("tabulator-block-select"),e.stopPropagation(),a.startY=void 0===e.screenY?e.touches[0].screenY:e.screenY,a.startHeight=t.getHeight(),document.body.addEventListener("mousemove",s),document.body.addEventListener("mouseup",n),o.addEventListener("touchmove",s,{passive:!0}),o.addEventListener("touchend",n)},Tabulator.prototype.registerModule("resizeRows",ResizeRows); -------------------------------------------------------------------------------- /resources/app/lib/js/tabulator/modules/resize_table.js: -------------------------------------------------------------------------------- 1 | /* Tabulator v4.7.1 (c) Oliver Folkerd */ 2 | 3 | var ResizeTable = function ResizeTable(table) { 4 | this.table = table; //hold Tabulator object 5 | this.binding = false; 6 | this.observer = false; 7 | this.containerObserver = false; 8 | 9 | this.tableHeight = 0; 10 | this.tableWidth = 0; 11 | this.containerHeight = 0; 12 | this.containerWidth = 0; 13 | 14 | this.autoResize = false; 15 | }; 16 | 17 | ResizeTable.prototype.initialize = function (row) { 18 | var _this = this; 19 | 20 | var table = this.table, 21 | tableStyle; 22 | 23 | this.tableHeight = table.element.clientHeight; 24 | this.tableWidth = table.element.clientWidth; 25 | 26 | if (table.element.parentNode) { 27 | this.containerHeight = table.element.parentNode.clientHeight; 28 | this.containerWidth = table.element.parentNode.clientWidth; 29 | } 30 | 31 | if (typeof ResizeObserver !== "undefined" && table.rowManager.getRenderMode() === "virtual") { 32 | 33 | this.autoResize = true; 34 | 35 | this.observer = new ResizeObserver(function (entry) { 36 | if (!table.browserMobile || table.browserMobile && !table.modules.edit.currentCell) { 37 | 38 | var nodeHeight = Math.floor(entry[0].contentRect.height); 39 | var nodeWidth = Math.floor(entry[0].contentRect.width); 40 | 41 | if (_this.tableHeight != nodeHeight || _this.tableWidth != nodeWidth) { 42 | _this.tableHeight = nodeHeight; 43 | _this.tableWidth = nodeWidth; 44 | 45 | if (table.element.parentNode) { 46 | _this.containerHeight = table.element.parentNode.clientHeight; 47 | _this.containerWidth = table.element.parentNode.clientWidth; 48 | } 49 | 50 | table.redraw(); 51 | } 52 | } 53 | }); 54 | 55 | this.observer.observe(table.element); 56 | 57 | tableStyle = window.getComputedStyle(table.element); 58 | 59 | if (this.table.element.parentNode && !this.table.rowManager.fixedHeight && (tableStyle.getPropertyValue("max-height") || tableStyle.getPropertyValue("min-height"))) { 60 | 61 | this.containerObserver = new ResizeObserver(function (entry) { 62 | if (!table.browserMobile || table.browserMobile && !table.modules.edit.currentCell) { 63 | 64 | var nodeHeight = Math.floor(entry[0].contentRect.height); 65 | var nodeWidth = Math.floor(entry[0].contentRect.width); 66 | 67 | if (_this.containerHeight != nodeHeight || _this.containerWidth != nodeWidth) { 68 | _this.containerHeight = nodeHeight; 69 | _this.containerWidth = nodeWidth; 70 | _this.tableHeight = table.element.clientHeight; 71 | _this.tableWidth = table.element.clientWidth; 72 | 73 | table.redraw(); 74 | } 75 | 76 | table.redraw(); 77 | } 78 | }); 79 | 80 | this.containerObserver.observe(this.table.element.parentNode); 81 | } 82 | } else { 83 | this.binding = function () { 84 | if (!table.browserMobile || table.browserMobile && !table.modules.edit.currentCell) { 85 | table.redraw(); 86 | } 87 | }; 88 | 89 | window.addEventListener("resize", this.binding); 90 | } 91 | }; 92 | 93 | ResizeTable.prototype.clearBindings = function (row) { 94 | if (this.binding) { 95 | window.removeEventListener("resize", this.binding); 96 | } 97 | 98 | if (this.observer) { 99 | this.observer.unobserve(this.table.element); 100 | } 101 | 102 | if (this.containerObserver) { 103 | this.containerObserver.unobserve(this.table.element.parentNode); 104 | } 105 | }; 106 | 107 | Tabulator.prototype.registerModule("resizeTable", ResizeTable); -------------------------------------------------------------------------------- /resources/app/lib/js/tabulator/modules/resize_table.min.js: -------------------------------------------------------------------------------- 1 | /* Tabulator v4.7.1 (c) Oliver Folkerd */ 2 | var ResizeTable=function(e){this.table=e,this.binding=!1,this.observer=!1,this.containerObserver=!1,this.tableHeight=0,this.tableWidth=0,this.containerHeight=0,this.containerWidth=0,this.autoResize=!1};ResizeTable.prototype.initialize=function(e){var t,i=this,n=this.table;this.tableHeight=n.element.clientHeight,this.tableWidth=n.element.clientWidth,n.element.parentNode&&(this.containerHeight=n.element.parentNode.clientHeight,this.containerWidth=n.element.parentNode.clientWidth),"undefined"!=typeof ResizeObserver&&"virtual"===n.rowManager.getRenderMode()?(this.autoResize=!0,this.observer=new ResizeObserver(function(e){if(!n.browserMobile||n.browserMobile&&!n.modules.edit.currentCell){var t=Math.floor(e[0].contentRect.height),r=Math.floor(e[0].contentRect.width);i.tableHeight==t&&i.tableWidth==r||(i.tableHeight=t,i.tableWidth=r,n.element.parentNode&&(i.containerHeight=n.element.parentNode.clientHeight,i.containerWidth=n.element.parentNode.clientWidth),n.redraw())}}),this.observer.observe(n.element),t=window.getComputedStyle(n.element),this.table.element.parentNode&&!this.table.rowManager.fixedHeight&&(t.getPropertyValue("max-height")||t.getPropertyValue("min-height"))&&(this.containerObserver=new ResizeObserver(function(e){if(!n.browserMobile||n.browserMobile&&!n.modules.edit.currentCell){var t=Math.floor(e[0].contentRect.height),r=Math.floor(e[0].contentRect.width);i.containerHeight==t&&i.containerWidth==r||(i.containerHeight=t,i.containerWidth=r,i.tableHeight=n.element.clientHeight,i.tableWidth=n.element.clientWidth,n.redraw()),n.redraw()}}),this.containerObserver.observe(this.table.element.parentNode))):(this.binding=function(){(!n.browserMobile||n.browserMobile&&!n.modules.edit.currentCell)&&n.redraw()},window.addEventListener("resize",this.binding))},ResizeTable.prototype.clearBindings=function(e){this.binding&&window.removeEventListener("resize",this.binding),this.observer&&this.observer.unobserve(this.table.element),this.containerObserver&&this.containerObserver.unobserve(this.table.element.parentNode)},Tabulator.prototype.registerModule("resizeTable",ResizeTable); -------------------------------------------------------------------------------- /resources/app/lib/js/tabulator/modules/responsive_layout.min.js: -------------------------------------------------------------------------------- 1 | /* Tabulator v4.7.1 (c) Oliver Folkerd */ 2 | var ResponsiveLayout=function(e){this.table=e,this.columns=[],this.hiddenColumns=[],this.mode="",this.index=0,this.collapseFormatter=[],this.collapseStartOpen=!0,this.collapseHandleColumn=!1};ResponsiveLayout.prototype.initialize=function(){var e=this,t=[];this.mode=this.table.options.responsiveLayout,this.collapseFormatter=this.table.options.responsiveLayoutCollapseFormatter||this.formatCollapsedData,this.collapseStartOpen=this.table.options.responsiveLayoutCollapseStartOpen,this.hiddenColumns=[],this.table.columnManager.columnsByIndex.forEach(function(o,n){o.modules.responsive&&o.modules.responsive.order&&o.modules.responsive.visible&&(o.modules.responsive.index=n,t.push(o),o.visible||"collapse"!==e.mode||e.hiddenColumns.push(o))}),t=t.reverse(),t=t.sort(function(e,t){return t.modules.responsive.order-e.modules.responsive.order||t.modules.responsive.index-e.modules.responsive.index}),this.columns=t,"collapse"===this.mode&&this.generateCollapsedContent();for(var o=this.table.columnManager.columnsByIndex,n=Array.isArray(o),s=0,o=n?o:o[Symbol.iterator]();;){var i;if(n){if(s>=o.length)break;i=o[s++]}else{if(s=o.next(),s.done)break;i=s.value}var l=i;if("responsiveCollapse"==l.definition.formatter){this.collapseHandleColumn=l;break}}this.collapseHandleColumn&&(this.hiddenColumns.length?this.collapseHandleColumn.show():this.collapseHandleColumn.hide())},ResponsiveLayout.prototype.initializeColumn=function(e){var t=e.getDefinition();e.modules.responsive={order:void 0===t.responsive?1:t.responsive,visible:!1!==t.visible}},ResponsiveLayout.prototype.initializeRow=function(e){var t;"calc"!==e.type&&(t=document.createElement("div"),t.classList.add("tabulator-responsive-collapse"),e.modules.responsiveLayout={element:t,open:this.collapseStartOpen},this.collapseStartOpen||(t.style.display="none"))},ResponsiveLayout.prototype.layoutRow=function(e){var t=e.getElement();e.modules.responsiveLayout&&(t.appendChild(e.modules.responsiveLayout.element),this.generateCollapsedRowContent(e))},ResponsiveLayout.prototype.updateColumnVisibility=function(e,t){e.modules.responsive&&(e.modules.responsive.visible=t,this.initialize())},ResponsiveLayout.prototype.hideColumn=function(e){var t=this.hiddenColumns.length;e.hide(!1,!0),"collapse"===this.mode&&(this.hiddenColumns.unshift(e),this.generateCollapsedContent(),this.collapseHandleColumn&&!t&&this.collapseHandleColumn.show())},ResponsiveLayout.prototype.showColumn=function(e){var t;e.show(!1,!0),e.setWidth(e.getWidth()),"collapse"===this.mode&&(t=this.hiddenColumns.indexOf(e),t>-1&&this.hiddenColumns.splice(t,1),this.generateCollapsedContent(),this.collapseHandleColumn&&!this.hiddenColumns.length&&this.collapseHandleColumn.hide())},ResponsiveLayout.prototype.update=function(){for(var e=this,t=!0;t;){var o="fitColumns"==e.table.modules.layout.getMode()?e.table.columnManager.getFlexBaseWidth():e.table.columnManager.getWidth(),n=(e.table.options.headerVisible?e.table.columnManager.element.clientWidth:e.table.element.clientWidth)-o;if(n<0){var s=e.columns[e.index];s?(e.hideColumn(s),e.index++):t=!1}else{var i=e.columns[e.index-1];i&&n>0&&n>=i.getWidth()?(e.showColumn(i),e.index--):t=!1}e.table.rowManager.activeRowsCount||e.table.rowManager.renderEmptyScroll()}},ResponsiveLayout.prototype.generateCollapsedContent=function(){var e=this;this.table.rowManager.getDisplayRows().forEach(function(t){e.generateCollapsedRowContent(t)})},ResponsiveLayout.prototype.generateCollapsedRowContent=function(e){var t,o;if(e.modules.responsiveLayout){for(t=e.modules.responsiveLayout.element;t.firstChild;)t.removeChild(t.firstChild);o=this.collapseFormatter(this.generateCollapsedRowData(e)),o&&t.appendChild(o)}},ResponsiveLayout.prototype.generateCollapsedRowData=function(e){var t,o=this,n=e.getData(),s=[];return this.hiddenColumns.forEach(function(i){var l=i.getFieldValue(n);i.definition.title&&i.field&&(i.modules.format&&o.table.options.responsiveLayoutCollapseUseFormatters?(t={value:!1,data:{},getValue:function(){return l},getData:function(){return n},getElement:function(){return document.createElement("div")},getRow:function(){return e.getComponent()},getColumn:function(){return i.getComponent()}},s.push({title:i.definition.title,value:i.modules.format.formatter.call(o.table.modules.format,t,i.modules.format.params)})):s.push({title:i.definition.title,value:l}))}),s},ResponsiveLayout.prototype.formatCollapsedData=function(e){var t=document.createElement("table"),o="";return e.forEach(function(e){var t=document.createElement("div");e.value instanceof Node&&(t.appendChild(e.value),e.value=t.innerHTML),o+=""+e.title+""+e.value+""}),t.innerHTML=o,Object.keys(e).length?t:""},Tabulator.prototype.registerModule("responsiveLayout",ResponsiveLayout); -------------------------------------------------------------------------------- /resources/app/lib/js/tabulator/modules/select_row.min.js: -------------------------------------------------------------------------------- 1 | /* Tabulator v4.7.1 (c) Oliver Folkerd */ 2 | var _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},SelectRow=function(e){this.table=e,this.selecting=!1,this.lastClickedRow=!1,this.selectPrev=[],this.selectedRows=[],this.headerCheckboxElement=null};SelectRow.prototype.clearSelectionData=function(e){this.selecting=!1,this.lastClickedRow=!1,this.selectPrev=[],this.selectedRows=[],e||this._rowSelectionChanged()},SelectRow.prototype.initializeRow=function(e){var t=this,o=e.getElement(),l=function e(){setTimeout(function(){t.selecting=!1},50),document.body.removeEventListener("mouseup",e)};e.modules.select={selected:!1},t.table.options.selectableCheck.call(this.table,e.getComponent())?(o.classList.add("tabulator-selectable"),o.classList.remove("tabulator-unselectable"),t.table.options.selectable&&"highlight"!=t.table.options.selectable&&("click"===t.table.options.selectableRangeMode?o.addEventListener("click",function(o){if(o.shiftKey){t.table._clearSelection(),t.lastClickedRow=t.lastClickedRow||e;var l=t.table.rowManager.getDisplayRowIndex(t.lastClickedRow),s=t.table.rowManager.getDisplayRowIndex(e),c=l<=s?l:s,i=l>=s?l:s,n=t.table.rowManager.getDisplayRows().slice(0),a=n.splice(c,i-c+1);o.ctrlKey||o.metaKey?(a.forEach(function(o){o!==t.lastClickedRow&&(!0===t.table.options.selectable||t.isRowSelected(e)?t.toggleRow(o):t.selectedRows.lengtht.table.options.selectable&&(a=a.slice(0,t.table.options.selectable)),t.selectRows(a)),t.table._clearSelection()}else o.ctrlKey||o.metaKey?(t.toggleRow(e),t.lastClickedRow=e):(t.deselectRows(void 0,!0),t.selectRows(e),t.lastClickedRow=e)}):(o.addEventListener("click",function(o){t.table.modExists("edit")&&t.table.modules.edit.getCurrentCell()||t.table._clearSelection(),t.selecting||t.toggleRow(e)}),o.addEventListener("mousedown",function(o){if(o.shiftKey)return t.table._clearSelection(),t.selecting=!0,t.selectPrev=[],document.body.addEventListener("mouseup",l),document.body.addEventListener("keyup",l),t.toggleRow(e),!1}),o.addEventListener("mouseenter",function(o){t.selecting&&(t.table._clearSelection(),t.toggleRow(e),t.selectPrev[1]==e&&t.toggleRow(t.selectPrev[0]))}),o.addEventListener("mouseout",function(o){t.selecting&&(t.table._clearSelection(),t.selectPrev.unshift(e))})))):(o.classList.add("tabulator-unselectable"),o.classList.remove("tabulator-selectable"))},SelectRow.prototype.toggleRow=function(e){this.table.options.selectableCheck.call(this.table,e.getComponent())&&(e.modules.select&&e.modules.select.selected?this._deselectRow(e):this._selectRow(e))},SelectRow.prototype.selectRows=function(e){var t,o=this;switch(void 0===e?"undefined":_typeof(e)){case"undefined":this.table.rowManager.rows.forEach(function(e){o._selectRow(e,!0,!0)}),this._rowSelectionChanged();break;case"string":t=this.table.rowManager.findRow(e),t?this._selectRow(t,!0,!0):this.table.rowManager.getRows(e).forEach(function(e){o._selectRow(e,!0,!0)}),this._rowSelectionChanged();break;default:Array.isArray(e)?(e.forEach(function(e){o._selectRow(e,!0,!0)}),this._rowSelectionChanged()):this._selectRow(e,!1,!0)}},SelectRow.prototype._selectRow=function(e,t,o){if(!isNaN(this.table.options.selectable)&&!0!==this.table.options.selectable&&!o&&this.selectedRows.length>=this.table.options.selectable){if(!this.table.options.selectableRollingSelection)return!1;this._deselectRow(this.selectedRows[0])}var l=this.table.rowManager.findRow(e);l?-1==this.selectedRows.indexOf(l)&&(l.modules.select||(l.modules.select={}),l.modules.select.selected=!0,l.modules.select.checkboxEl&&(l.modules.select.checkboxEl.checked=!0),l.getElement().classList.add("tabulator-selected"),this.selectedRows.push(l),this.table.options.dataTreeSelectPropagate&&this.childRowSelection(l,!0),t||this.table.options.rowSelected.call(this.table,l.getComponent()),this._rowSelectionChanged(t)):t||console.warn("Selection Error - No such row found, ignoring selection:"+e)},SelectRow.prototype.isRowSelected=function(e){return-1!==this.selectedRows.indexOf(e)},SelectRow.prototype.deselectRows=function(e,t){var o,l=this;if(void 0===e){o=l.selectedRows.length;for(var s=0;s-1&&(s.modules.select||(s.modules.select={}),s.modules.select.selected=!1,s.modules.select.checkboxEl&&(s.modules.select.checkboxEl.checked=!1),s.getElement().classList.remove("tabulator-selected"),l.selectedRows.splice(o,1),this.table.options.dataTreeSelectPropagate&&this.childRowSelection(s,!1),t||l.table.options.rowDeselected.call(this.table,s.getComponent()),l._rowSelectionChanged(t)):t||console.warn("Deselection Error - No such row found, ignoring selection:"+e)},SelectRow.prototype.getSelectedData=function(){var e=[];return this.selectedRows.forEach(function(t){e.push(t.getData())}),e},SelectRow.prototype.getSelectedRows=function(){var e=[];return this.selectedRows.forEach(function(t){e.push(t.getComponent())}),e},SelectRow.prototype._rowSelectionChanged=function(e){this.headerCheckboxElement&&(0===this.selectedRows.length?(this.headerCheckboxElement.checked=!1,this.headerCheckboxElement.indeterminate=!1):this.table.rowManager.rows.length===this.selectedRows.length?(this.headerCheckboxElement.checked=!0,this.headerCheckboxElement.indeterminate=!1):(this.headerCheckboxElement.indeterminate=!0,this.headerCheckboxElement.checked=!1)),e||this.table.options.rowSelectionChanged.call(this.table,this.getSelectedData(),this.getSelectedRows())},SelectRow.prototype.registerRowSelectCheckbox=function(e,t){e._row.modules.select||(e._row.modules.select={}),e._row.modules.select.checkboxEl=t},SelectRow.prototype.registerHeaderSelectCheckbox=function(e){this.headerCheckboxElement=e},SelectRow.prototype.childRowSelection=function(e,t){var o=this.table.modules.dataTree.getChildren(e);if(t)for(var l=o,s=Array.isArray(l),c=0,l=s?l:l[Symbol.iterator]();;){var i;if(s){if(c>=l.length)break;i=l[c++]}else{if(c=l.next(),c.done)break;i=c.value}var n=i;this._selectRow(n,!0)}else for(var a=o,r=Array.isArray(a),d=0,a=r?a:a[Symbol.iterator]();;){var h;if(r){if(d>=a.length)break;h=a[d++]}else{if(d=a.next(),d.done)break;h=d.value}var w=h;this._deselectRow(w,!0)}},Tabulator.prototype.registerModule("selectRow",SelectRow); -------------------------------------------------------------------------------- /resources/app/lib/js/tabulator/modules/validate.min.js: -------------------------------------------------------------------------------- 1 | /* Tabulator v4.7.1 (c) Oliver Folkerd */ 2 | var _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Validate=function(t){this.table=t,this.invalidCells=[]};Validate.prototype.initializeColumn=function(t){var i,n=this,e=[];t.definition.validator&&(Array.isArray(t.definition.validator)?t.definition.validator.forEach(function(t){(i=n._extractValidator(t))&&e.push(i)}):(i=this._extractValidator(t.definition.validator))&&e.push(i),t.modules.validate=!!e.length&&e)},Validate.prototype._extractValidator=function(t){var i,n,e;switch(void 0===t?"undefined":_typeof(t)){case"string":return e=t.indexOf(":"),e>-1?(i=t.substring(0,e),n=t.substring(e+1)):i=t,this._buildValidator(i,n);case"function":return this._buildValidator(t);case"object":return this._buildValidator(t.type,t.parameters)}},Validate.prototype._buildValidator=function(t,i){var n="function"==typeof t?t:this.validators[t];return n?{type:"function"==typeof t?"function":t,func:n,params:i}:(console.warn("Validator Setup Error - No matching validator found:",t),!1)},Validate.prototype.validate=function(t,i,n){var e=this,a=[],o=this.invalidCells.indexOf(i);return t&&t.forEach(function(t){t.func.call(e,i.getComponent(),n,t.params)||a.push({type:t.type,parameters:t.params})}),a=!a.length||a,i.modules.validate||(i.modules.validate={}),!0===a?(i.modules.validate.invalid=!1,i.getElement().classList.remove("tabulator-validation-fail"),o>-1&&this.invalidCells.splice(o,1)):(i.modules.validate.invalid=!0,"manual"!==this.table.options.validationMode&&i.getElement().classList.add("tabulator-validation-fail"),-1==o&&this.invalidCells.push(i)),a},Validate.prototype.getInvalidCells=function(){var t=[];return this.invalidCells.forEach(function(i){t.push(i.getComponent())}),t},Validate.prototype.clearValidation=function(t){var i;t.modules.validate&&t.modules.validate.invalid&&(t.element.classList.remove("tabulator-validation-fail"),t.modules.validate.invalid=!1,(i=this.invalidCells.indexOf(t))>-1&&this.invalidCells.splice(i,1))},Validate.prototype.validators={integer:function(t,i,n){return""===i||null===i||void 0===i||"number"==typeof(i=Number(i))&&isFinite(i)&&Math.floor(i)===i},float:function(t,i,n){return""===i||null===i||void 0===i||"number"==typeof(i=Number(i))&&isFinite(i)&&i%1!=0},numeric:function(t,i,n){return""===i||null===i||void 0===i||!isNaN(i)},string:function(t,i,n){return""===i||null===i||void 0===i||isNaN(i)},max:function(t,i,n){return""===i||null===i||void 0===i||parseFloat(i)<=n},min:function(t,i,n){return""===i||null===i||void 0===i||parseFloat(i)>=n},starts:function(t,i,n){return""===i||null===i||void 0===i||String(i).toLowerCase().startsWith(String(n).toLowerCase())},ends:function(t,i,n){return""===i||null===i||void 0===i||String(i).toLowerCase().endsWith(String(n).toLowerCase())},minLength:function(t,i,n){return""===i||null===i||void 0===i||String(i).length>=n},maxLength:function(t,i,n){return""===i||null===i||void 0===i||String(i).length<=n},in:function(t,i,n){return""===i||null===i||void 0===i||("string"==typeof n&&(n=n.split("|")),""===i||n.indexOf(i)>-1)},regex:function(t,i,n){return""===i||null===i||void 0===i||new RegExp(n).test(i)},unique:function(t,i,n){if(""===i||null===i||void 0===i)return!0;var e=!0,a=t.getData(),o=t.getColumn()._getSelf();return this.table.rowManager.rows.forEach(function(t){var n=t.getData();n!==a&&i==o.getFieldValue(n)&&(e=!1)}),e},required:function(t,i,n){return""!==i&&null!==i&&void 0!==i}},Tabulator.prototype.registerModule("validate",Validate); -------------------------------------------------------------------------------- /resources/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giwty/switch-library-manager/f69daa7e5e3bc9fc2cc17ad839a12ac3bf5e0372/resources/icon.icns -------------------------------------------------------------------------------- /resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giwty/switch-library-manager/f69daa7e5e3bc9fc2cc17ad839a12ac3bf5e0372/resources/icon.ico -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giwty/switch-library-manager/f69daa7e5e3bc9fc2cc17ad839a12ac3bf5e0372/resources/icon.png -------------------------------------------------------------------------------- /settings/keys.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "errors" 5 | "github.com/magiconair/properties" 6 | "path/filepath" 7 | ) 8 | 9 | var ( 10 | keysInstance *switchKeys 11 | ) 12 | 13 | type switchKeys struct { 14 | keys map[string]string 15 | } 16 | 17 | func (k *switchKeys) GetKey(keyName string) string { 18 | return k.keys[keyName] 19 | } 20 | 21 | func SwitchKeys() (*switchKeys, error) { 22 | return keysInstance, nil 23 | } 24 | 25 | func InitSwitchKeys(baseFolder string) (*switchKeys, error) { 26 | 27 | // init from a file 28 | path := filepath.Join(baseFolder, "prod.keys") 29 | p, err := properties.LoadFile(path, properties.UTF8) 30 | if err != nil { 31 | path = "${HOME}/.switch/prod.keys" 32 | p, err = properties.LoadFile(path, properties.UTF8) 33 | } 34 | settings := ReadSettings(baseFolder) 35 | if err != nil { 36 | path := settings.Prodkeys 37 | if path != "" { 38 | p, err = properties.LoadFile(filepath.Join(path, "prod.keys"), properties.UTF8) 39 | } 40 | } 41 | if err != nil { 42 | return nil, errors.New("Error trying to read prod.keys [reason:" + err.Error() + "]") 43 | } 44 | settings.Prodkeys = path 45 | SaveSettings(settings, baseFolder) 46 | keysInstance = &switchKeys{keys: map[string]string{}} 47 | for _, key := range p.Keys() { 48 | value, _ := p.Get(key) 49 | keysInstance.keys[key] = value 50 | } 51 | 52 | return keysInstance, nil 53 | } 54 | -------------------------------------------------------------------------------- /settings/settings.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/mcuadros/go-version" 7 | "go.uber.org/zap" 8 | "io/ioutil" 9 | "net/http" 10 | "os" 11 | "path/filepath" 12 | ) 13 | 14 | var ( 15 | settingsInstance *AppSettings 16 | ) 17 | 18 | const ( 19 | SETTINGS_FILENAME = "settings.json" 20 | TITLE_JSON_FILENAME = "titles.json" 21 | VERSIONS_JSON_FILENAME = "versions.json" 22 | SLM_VERSION = "1.4.0" 23 | TITLES_JSON_URL = "https://tinfoil.media/repo/db/titles.json" 24 | //TITLES_JSON_URL = "https://raw.githubusercontent.com/blawar/titledb/master/titles.US.en.json" 25 | VERSIONS_JSON_URL = "https://tinfoil.media/repo/db/versions.json" 26 | //VERSIONS_JSON_URL = "https://raw.githubusercontent.com/blawar/titledb/master/versions.json" 27 | SLM_VERSION_URL = "https://raw.githubusercontent.com/giwty/switch-library-manager/master/slm.json" 28 | ) 29 | 30 | const ( 31 | TEMPLATE_TITLE_ID = "TITLE_ID" 32 | TEMPLATE_TITLE_NAME = "TITLE_NAME" 33 | TEMPLATE_DLC_NAME = "DLC_NAME" 34 | TEMPLATE_VERSION = "VERSION" 35 | TEMPLATE_REGION = "REGION" 36 | TEMPLATE_VERSION_TXT = "VERSION_TXT" 37 | TEMPLATE_TYPE = "TYPE" 38 | ) 39 | 40 | type OrganizeOptions struct { 41 | CreateFolderPerGame bool `json:"create_folder_per_game"` 42 | RenameFiles bool `json:"rename_files"` 43 | DeleteEmptyFolders bool `json:"delete_empty_folders"` 44 | DeleteOldUpdateFiles bool `json:"delete_old_update_files"` 45 | FolderNameTemplate string `json:"folder_name_template"` 46 | SwitchSafeFileNames bool `json:"switch_safe_file_names"` 47 | FileNameTemplate string `json:"file_name_template"` 48 | } 49 | 50 | type AppSettings struct { 51 | VersionsEtag string `json:"versions_etag"` 52 | TitlesEtag string `json:"titles_etag"` 53 | Prodkeys string `json:"prod_keys"` 54 | Folder string `json:"folder"` 55 | ScanFolders []string `json:"scan_folders"` 56 | GUI bool `json:"gui"` 57 | Debug bool `json:"debug"` 58 | CheckForMissingUpdates bool `json:"check_for_missing_updates"` 59 | CheckForMissingDLC bool `json:"check_for_missing_dlc"` 60 | OrganizeOptions OrganizeOptions `json:"organize_options"` 61 | ScanRecursively bool `json:"scan_recursively"` 62 | GuiPagingSize int `json:"gui_page_size"` 63 | IgnoreDLCTitleIds []string `json:"ignore_dlc_title_ids"` 64 | } 65 | 66 | func ReadSettingsAsJSON(baseFolder string) string { 67 | if _, err := os.Stat(filepath.Join(baseFolder, SETTINGS_FILENAME)); err != nil { 68 | saveDefaultSettings(baseFolder) 69 | } 70 | file, _ := os.Open(filepath.Join(baseFolder, SETTINGS_FILENAME)) 71 | bytes, _ := ioutil.ReadAll(file) 72 | return string(bytes) 73 | } 74 | 75 | func ReadSettings(baseFolder string) *AppSettings { 76 | if settingsInstance != nil { 77 | return settingsInstance 78 | } 79 | settingsInstance = &AppSettings{Debug: false, GuiPagingSize: 100, ScanFolders: []string{}, 80 | OrganizeOptions: OrganizeOptions{SwitchSafeFileNames: true}, Prodkeys: "", IgnoreDLCTitleIds: []string{"01007F600B135007"}} 81 | if _, err := os.Stat(filepath.Join(baseFolder, SETTINGS_FILENAME)); err == nil { 82 | file, err := os.Open(filepath.Join(baseFolder, SETTINGS_FILENAME)) 83 | if err != nil { 84 | zap.S().Warnf("Missing or corrupted config file, creating a new one") 85 | return saveDefaultSettings(baseFolder) 86 | } else { 87 | _ = json.NewDecoder(file).Decode(&settingsInstance) 88 | return settingsInstance 89 | } 90 | } else { 91 | return saveDefaultSettings(baseFolder) 92 | } 93 | } 94 | 95 | func saveDefaultSettings(baseFolder string) *AppSettings { 96 | settingsInstance = &AppSettings{ 97 | TitlesEtag: "W/\"a5b02845cf6bd61:0\"", 98 | VersionsEtag: "W/\"2ef50d1cb6bd61:0\"", 99 | Folder: "", 100 | ScanFolders: []string{}, 101 | IgnoreDLCTitleIds: []string{}, 102 | GUI: true, 103 | GuiPagingSize: 100, 104 | CheckForMissingUpdates: true, 105 | CheckForMissingDLC: true, 106 | ScanRecursively: true, 107 | Debug: false, 108 | OrganizeOptions: OrganizeOptions{ 109 | RenameFiles: false, 110 | CreateFolderPerGame: false, 111 | FolderNameTemplate: fmt.Sprintf("{%v}", TEMPLATE_TITLE_NAME), 112 | FileNameTemplate: fmt.Sprintf("{%v} ({%v})[{%v}][v{%v}]", TEMPLATE_TITLE_NAME, TEMPLATE_DLC_NAME, 113 | TEMPLATE_TITLE_ID, TEMPLATE_VERSION), 114 | DeleteEmptyFolders: false, 115 | SwitchSafeFileNames: true, 116 | DeleteOldUpdateFiles: false, 117 | }, 118 | } 119 | return SaveSettings(settingsInstance, baseFolder) 120 | } 121 | 122 | func SaveSettings(settings *AppSettings, baseFolder string) *AppSettings { 123 | file, _ := json.MarshalIndent(settings, "", " ") 124 | _ = ioutil.WriteFile(filepath.Join(baseFolder, SETTINGS_FILENAME), file, 0644) 125 | settingsInstance = settings 126 | return settings 127 | } 128 | 129 | func CheckForUpdates() (bool, error) { 130 | 131 | localVer := SLM_VERSION 132 | 133 | res, err := http.Get(SLM_VERSION_URL) 134 | if err != nil { 135 | return false, err 136 | } 137 | defer res.Body.Close() 138 | 139 | body, err := ioutil.ReadAll(res.Body) 140 | if err != nil { 141 | return false, err 142 | } 143 | 144 | remoteValues := map[string]string{} 145 | err = json.Unmarshal(body, &remoteValues) 146 | if err != nil { 147 | return false, err 148 | } 149 | 150 | remoteVer := remoteValues["version"] 151 | 152 | if version.CompareSimple(remoteVer, localVer) > 0 { 153 | return true, nil 154 | } 155 | 156 | return false, nil 157 | } 158 | -------------------------------------------------------------------------------- /slm.json: -------------------------------------------------------------------------------- 1 | { 2 | "version":"1.4.0" 3 | } -------------------------------------------------------------------------------- /switchfs/_crypto/ecb.go: -------------------------------------------------------------------------------- 1 | package _crypto 2 | 3 | import "crypto/aes" 4 | 5 | func DecryptAes128Ecb(data, key []byte) []byte { 6 | 7 | cipher, _ := aes.NewCipher([]byte(key)) 8 | decrypted := make([]byte, len(data)) 9 | size := 16 10 | 11 | for bs, be := 0, size; bs < len(data); bs, be = bs+size, be+size { 12 | cipher.Decrypt(decrypted[bs:be], data[bs:be]) 13 | } 14 | 15 | return decrypted 16 | } 17 | -------------------------------------------------------------------------------- /switchfs/_crypto/xts.go: -------------------------------------------------------------------------------- 1 | /// Modified xts to support custom tweak 2 | 3 | // Copyright 2012 The Go Authors. All rights reserved. 4 | // Use of this source code is governed by a BSD-style 5 | // license that can be found in the LICENSE file. 6 | 7 | // Package xts implements the XTS cipher mode as specified in IEEE P1619/D16. 8 | // 9 | // XTS mode is typically used for disk encryption, which presents a number of 10 | // novel problems that make more common modes inapplicable. The disk is 11 | // conceptually an array of sectors and we must be able to encrypt and decrypt 12 | // a sector in isolation. However, an attacker must not be able to transpose 13 | // two sectors of plaintext by transposing their ciphertext. 14 | // 15 | // XTS wraps a block cipher with Rogaway's XEX mode in order to build a 16 | // tweakable block cipher. This allows each sector to have a unique tweak and 17 | // effectively create a unique key for each sector. 18 | // 19 | // XTS does not provide any authentication. An attacker can manipulate the 20 | // ciphertext and randomise a block (16 bytes) of the plaintext. This package 21 | // does not implement ciphertext-stealing so sectors must be a multiple of 16 22 | // bytes. 23 | // 24 | // Note that XTS is usually not appropriate for any use besides disk encryption. 25 | // Most users should use an AEAD mode like GCM (from crypto/cipher.NewGCM) instead. 26 | package _crypto 27 | 28 | import ( 29 | "crypto/cipher" 30 | "encoding/binary" 31 | "errors" 32 | "sync" 33 | "unsafe" 34 | ) 35 | 36 | // Cipher contains an expanded key structure. It is safe for concurrent use if 37 | // the underlying block cipher is safe for concurrent use. 38 | type Cipher struct { 39 | k1, k2 cipher.Block 40 | } 41 | 42 | // blockSize is the block size that the underlying cipher must have. XTS is 43 | // only defined for 16-byte ciphers. 44 | const blockSize = 16 45 | 46 | var tweakPool = sync.Pool{ 47 | New: func() interface{} { 48 | return new([blockSize]byte) 49 | }, 50 | } 51 | 52 | // NewCipher creates a Cipher given a function for creating the underlying 53 | // block cipher (which must have a block size of 16 bytes). The key must be 54 | // twice the length of the underlying cipher's key. 55 | func NewCipher(cipherFunc func([]byte) (cipher.Block, error), key []byte) (c *Cipher, err error) { 56 | c = new(Cipher) 57 | if c.k1, err = cipherFunc(key[:len(key)/2]); err != nil { 58 | return 59 | } 60 | c.k2, err = cipherFunc(key[len(key)/2:]) 61 | 62 | if c.k1.BlockSize() != blockSize { 63 | err = errors.New("xts: cipher does not have a block size of 16") 64 | } 65 | 66 | return 67 | } 68 | 69 | // Encrypt encrypts a sector of plaintext and puts the result into ciphertext. 70 | // Plaintext and ciphertext must overlap entirely or not at all. 71 | // Sectors must be a multiple of 16 bytes and less than 2²⁴ bytes. 72 | func (c *Cipher) Encrypt(ciphertext, plaintext []byte, sectorNum uint64) { 73 | if len(ciphertext) < len(plaintext) { 74 | panic("xts: ciphertext is smaller than plaintext") 75 | } 76 | if len(plaintext)%blockSize != 0 { 77 | panic("xts: plaintext is not a multiple of the block size") 78 | } 79 | if InexactOverlap(ciphertext[:len(plaintext)], plaintext) { 80 | panic("xts: invalid buffer overlap") 81 | } 82 | 83 | tweak := tweakPool.Get().(*[blockSize]byte) 84 | for i := range tweak { 85 | tweak[i] = 0 86 | } 87 | binary.LittleEndian.PutUint64(tweak[:8], sectorNum) 88 | 89 | c.k2.Encrypt(tweak[:], tweak[:]) 90 | 91 | for len(plaintext) > 0 { 92 | for j := range tweak { 93 | ciphertext[j] = plaintext[j] ^ tweak[j] 94 | } 95 | c.k1.Encrypt(ciphertext, ciphertext) 96 | for j := range tweak { 97 | ciphertext[j] ^= tweak[j] 98 | } 99 | plaintext = plaintext[blockSize:] 100 | ciphertext = ciphertext[blockSize:] 101 | 102 | mul2(tweak) 103 | } 104 | 105 | tweakPool.Put(tweak) 106 | } 107 | 108 | // Decrypt decrypts a sector of ciphertext and puts the result into plaintext. 109 | // Plaintext and ciphertext must overlap entirely or not at all. 110 | // Sectors must be a multiple of 16 bytes and less than 2²⁴ bytes. 111 | func (c *Cipher) Decrypt(plaintext, ciphertext []byte, tweak *[16]byte) { 112 | if len(plaintext) < len(ciphertext) { 113 | panic("xts: plaintext is smaller than ciphertext") 114 | } 115 | if len(ciphertext)%blockSize != 0 { 116 | panic("xts: ciphertext is not a multiple of the block size") 117 | } 118 | if InexactOverlap(plaintext[:len(ciphertext)], ciphertext) { 119 | panic("xts: invalid buffer overlap") 120 | } 121 | 122 | /* 123 | tweak := tweakPool.Get().(*[blockSize]byte) 124 | for i := range tweak { 125 | tweak[i] = 0 126 | } 127 | binary.LittleEndian.PutUint64(tweak[:8], sectorNum) 128 | */ 129 | c.k2.Encrypt(tweak[:], tweak[:]) 130 | for len(ciphertext) > 0 { 131 | for j := range tweak { 132 | plaintext[j] = ciphertext[j] ^ tweak[j] 133 | } 134 | c.k1.Decrypt(plaintext, plaintext) 135 | for j := range tweak { 136 | plaintext[j] ^= tweak[j] 137 | } 138 | plaintext = plaintext[blockSize:] 139 | ciphertext = ciphertext[blockSize:] 140 | 141 | mul2(tweak) 142 | } 143 | 144 | //tweakPool.Put(tweak) 145 | } 146 | 147 | // mul2 multiplies tweak by 2 in GF(2¹²⁸) with an irreducible polynomial of 148 | // x¹²⁸ + x⁷ + x² + x + 1. 149 | func mul2(tweak *[blockSize]byte) { 150 | var carryIn byte 151 | for j := range tweak { 152 | carryOut := tweak[j] >> 7 153 | tweak[j] = (tweak[j] << 1) + carryIn 154 | carryIn = carryOut 155 | } 156 | if carryIn != 0 { 157 | // If we have a carry bit then we need to subtract a multiple 158 | // of the irreducible polynomial (x¹²⁸ + x⁷ + x² + x + 1). 159 | // By dropping the carry bit, we're subtracting the x^128 term 160 | // so all that remains is to subtract x⁷ + x² + x + 1. 161 | // Subtraction (and addition) in this representation is just 162 | // XOR. 163 | tweak[0] ^= 1<<7 | 1<<2 | 1<<1 | 1 164 | } 165 | } 166 | 167 | func AnyOverlap(x, y []byte) bool { 168 | return len(x) > 0 && len(y) > 0 && 169 | uintptr(unsafe.Pointer(&x[0])) <= uintptr(unsafe.Pointer(&y[len(y)-1])) && 170 | uintptr(unsafe.Pointer(&y[0])) <= uintptr(unsafe.Pointer(&x[len(x)-1])) 171 | } 172 | 173 | // InexactOverlap reports whether x and y share memory at any non-corresponding 174 | // index. The memory beyond the slice length is ignored. Note that x and y can 175 | // have different lengths and still not have any inexact overlap. 176 | // 177 | // InexactOverlap can be used to implement the requirements of the crypto/cipher 178 | // AEAD, Block, BlockMode and Stream interfaces. 179 | func InexactOverlap(x, y []byte) bool { 180 | if len(x) == 0 || len(y) == 0 || &x[0] == &y[0] { 181 | return false 182 | } 183 | return AnyOverlap(x, y) 184 | } 185 | -------------------------------------------------------------------------------- /switchfs/cnmt.go: -------------------------------------------------------------------------------- 1 | package switchfs 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/xml" 6 | "errors" 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | const ( 12 | ContentMetaType_SystemProgram = 1 13 | ContentMetaType_SystemData = 2 14 | ContentMetaType_SystemUpdate = 3 15 | ContentMetaType_BootImagePackage = 4 16 | ContentMetaType_BootImagePackageSafe = 5 17 | ContentMetaType_Application = 0x80 18 | ContentMetaType_Patch = 0x81 19 | ContentMetaType_AddOnContent = 0x82 20 | ContentMetaType_Delta = 0x83 21 | ) 22 | 23 | type Content struct { 24 | Text string `xml:",chardata"` 25 | Type string `xml:"Type"` 26 | ID string `xml:"Id"` 27 | Size string `xml:"Size"` 28 | Hash string `xml:"Hash"` 29 | KeyGeneration string `xml:"KeyGeneration"` 30 | } 31 | 32 | type ContentMetaAttributes struct { 33 | TitleId string `json:"title_id"` 34 | Version int `json:"version"` 35 | Type string `json:"type"` 36 | Contents map[string]Content 37 | Ncap *Nacp 38 | } 39 | 40 | type ContentMeta struct { 41 | XMLName xml.Name `xml:"ContentMeta"` 42 | Text string `xml:",chardata"` 43 | Type string `xml:"Type"` 44 | ID string `xml:"Id"` 45 | Version int `xml:"Version"` 46 | RequiredDownloadSystemVersion string `xml:"RequiredDownloadSystemVersion"` 47 | Content []struct { 48 | Text string `xml:",chardata"` 49 | Type string `xml:"Type"` 50 | ID string `xml:"Id"` 51 | Size string `xml:"Size"` 52 | Hash string `xml:"Hash"` 53 | KeyGeneration string `xml:"KeyGeneration"` 54 | } `xml:"Content"` 55 | Digest string `xml:"Digest"` 56 | KeyGenerationMin string `xml:"KeyGenerationMin"` 57 | RequiredSystemVersion string `xml:"RequiredSystemVersion"` 58 | OriginalId string `xml:"OriginalId"` 59 | } 60 | 61 | func readBinaryCnmt(pfs0 *PFS0, data []byte) (*ContentMetaAttributes, error) { 62 | if pfs0 == nil || len(pfs0.Files) != 1 { 63 | return nil, errors.New("unexpected pfs0") 64 | } 65 | cnmtFile := pfs0.Files[0] 66 | cnmt := data[int64(cnmtFile.StartOffset):] 67 | titleId := binary.LittleEndian.Uint64(cnmt[0:0x8]) 68 | version := binary.LittleEndian.Uint32(cnmt[0x8:0xC]) 69 | tableOffset := binary.LittleEndian.Uint16(cnmt[0xE:0x10]) 70 | contentEntryCount := binary.LittleEndian.Uint16(cnmt[0x10:0x12]) 71 | //metaEntryCount := binary.LittleEndian.Uint16(cnmt[0x12:0x14]) 72 | contents := map[string]Content{} 73 | for i := uint16(0); i < contentEntryCount; i++ { 74 | position := 0x20 /*size of cnmt header*/ + tableOffset + (i * uint16(0x38)) 75 | ncaId := cnmt[position+0x20 : position+0x20+0x10] 76 | //fmt.Println(fmt.Sprintf("0%x", ncaId)) 77 | contentType := "" 78 | switch cnmt[position+0x36 : position+0x36+1][0] { 79 | case 0: 80 | contentType = "Meta" 81 | case 1: 82 | contentType = "Program" 83 | case 2: 84 | contentType = "Data" 85 | case 3: 86 | contentType = "Control" 87 | case 4: 88 | contentType = "HtmlDocument" 89 | case 5: 90 | contentType = "LegalInformation" 91 | case 6: 92 | contentType = "DeltaFragment" 93 | } 94 | contents[contentType] = Content{ID: fmt.Sprintf("%x", ncaId)} 95 | } 96 | metaType := "" 97 | switch cnmt[0xC:0xD][0] { 98 | case ContentMetaType_Application: 99 | metaType = "BASE" 100 | case ContentMetaType_AddOnContent: 101 | metaType = "DLC" 102 | case ContentMetaType_Patch: 103 | metaType = "UPD" 104 | } 105 | 106 | return &ContentMetaAttributes{Contents: contents, Version: int(version), TitleId: fmt.Sprintf("0%x", titleId), Type: metaType}, nil 107 | } 108 | 109 | func readXmlCnmt(xmlBytes []byte) (*ContentMetaAttributes, error) { 110 | cmt := &ContentMeta{} 111 | err := xml.Unmarshal(xmlBytes, &cmt) 112 | if err != nil { 113 | return nil, err 114 | } 115 | titleId := strings.Replace(cmt.ID, "0x", "", 1) 116 | return &ContentMetaAttributes{Version: cmt.Version, TitleId: titleId, Type: cmt.Type}, nil 117 | } 118 | -------------------------------------------------------------------------------- /switchfs/fs.go: -------------------------------------------------------------------------------- 1 | package switchfs 2 | 3 | import ( 4 | "bytes" 5 | "crypto/sha256" 6 | "encoding/binary" 7 | "errors" 8 | ) 9 | 10 | type fsHeader struct { 11 | encType byte //(0 = Auto, 1 = None, 2 = AesCtrOld, 3 = AesCtr, 4 = AesCtrEx) 12 | fsType byte //(0 = RomFs, 1 = PartitionFs) 13 | hashType byte // (0 = Auto, 2 = HierarchicalSha256, 3 = HierarchicalIntegrity (Ivfc)) 14 | fsHeaderBytes []byte 15 | generation uint32 16 | } 17 | 18 | type fsEntry struct { 19 | StartOffset uint32 20 | EndOffset uint32 21 | Size uint32 22 | } 23 | 24 | type hashInfo struct { 25 | pfs0HeaderOffset uint64 26 | pfs0size uint64 27 | } 28 | 29 | func getFsEntry(ncaHeader *ncaHeader, index int) fsEntry { 30 | fsEntryOffset := 0x240 + 0x10*index 31 | fsEntryBytes := ncaHeader.headerBytes[fsEntryOffset : fsEntryOffset+0x10] 32 | 33 | entryStartOffset := binary.LittleEndian.Uint32(fsEntryBytes[0x0:0x4]) * 0x200 34 | entryEndOffset := binary.LittleEndian.Uint32(fsEntryBytes[0x4:0x8]) * 0x200 35 | 36 | return fsEntry{StartOffset: entryStartOffset, EndOffset: entryEndOffset, Size: entryEndOffset - entryStartOffset} 37 | } 38 | 39 | func getFsHeader(ncaHeader *ncaHeader, index int) (*fsHeader, error) { 40 | fsHeaderHashOffset := /*hash pfs0HeaderOffset*/ 0x280 + /*hash pfs0size*/ 0x20*index 41 | fsHeaderHash := ncaHeader.headerBytes[fsHeaderHashOffset : fsHeaderHashOffset+0x20] 42 | 43 | fsHeaderOffset := 0x400 + 0x200*index 44 | fsHeaderBytes := ncaHeader.headerBytes[fsHeaderOffset : fsHeaderOffset+0x200] 45 | 46 | actualHash := sha256.Sum256(fsHeaderBytes) 47 | 48 | if bytes.Compare(actualHash[:], fsHeaderHash) != 0 { 49 | return nil, errors.New("fs headerBytes hash mismatch") 50 | } 51 | 52 | result := fsHeader{fsHeaderBytes: fsHeaderBytes} 53 | 54 | result.fsType = fsHeaderBytes[0x2:0x3][0] 55 | result.hashType = fsHeaderBytes[0x3:0x4][0] 56 | result.encType = fsHeaderBytes[0x4:0x5][0] 57 | 58 | generationBytes := fsHeaderBytes[0x140 : 0x140+0x4] //generation 59 | result.generation = binary.LittleEndian.Uint32(generationBytes) 60 | 61 | return &result, nil 62 | } 63 | 64 | func (fh *fsHeader) getHashInfo() (*hashInfo, error) { 65 | hashInfoBytes := fh.fsHeaderBytes[0x8:0x100] 66 | result := hashInfo{} 67 | if fh.hashType == 2 { 68 | 69 | result.pfs0HeaderOffset = binary.LittleEndian.Uint64(hashInfoBytes[0x38 : 0x38+0x8]) 70 | result.pfs0size = binary.LittleEndian.Uint64(hashInfoBytes[0x40 : 0x40+0x8]) 71 | return &result, nil 72 | } else if fh.hashType == 3 { 73 | result.pfs0HeaderOffset = binary.LittleEndian.Uint64(hashInfoBytes[0x88 : 0x88+0x8]) 74 | result.pfs0size = binary.LittleEndian.Uint64(hashInfoBytes[0x90 : 0x90+0x8]) 75 | return &result, nil 76 | } 77 | return nil, errors.New("non supported hash type") 78 | } 79 | -------------------------------------------------------------------------------- /switchfs/nacp.go: -------------------------------------------------------------------------------- 1 | package switchfs 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "io" 7 | ) 8 | 9 | type Language int 10 | 11 | const ( 12 | AmericanEnglish = iota 13 | BritishEnglish 14 | Japanese 15 | French 16 | German 17 | LatinAmericanSpanish 18 | Spanish 19 | Italian 20 | Dutch 21 | CanadianFrench 22 | Portuguese 23 | Russian 24 | Korean 25 | Taiwanese 26 | Chinese 27 | ) 28 | 29 | type NacpTitle struct { 30 | Language Language 31 | Title string 32 | } 33 | 34 | type Nacp struct { 35 | TitleName map[string]NacpTitle 36 | Isbn string 37 | DisplayVersion string 38 | SupportedLanguageFlag uint32 39 | } 40 | 41 | func (l Language) String() string { 42 | return [...]string{ 43 | "AmericanEnglish", 44 | "BritishEnglish", 45 | "Japanese", 46 | "French", 47 | "German", 48 | "LatinAmericanSpanish", 49 | "Spanish", 50 | "Italian", 51 | "Dutch", 52 | "CanadianFrench", 53 | "Portuguese", 54 | "Russian", 55 | "Korean", 56 | "Taiwanese", 57 | "Chinese", 58 | "Chinese"}[l] 59 | } 60 | 61 | func ExtractNacp(cnmt *ContentMetaAttributes, file io.ReaderAt, securePartition *PFS0, securePartitionOffset int64) (*Nacp, error) { 62 | if control, ok := cnmt.Contents["Control"]; ok { 63 | controlNca := getNcaById(securePartition, control.ID) 64 | if controlNca != nil { 65 | fsHeader, section, err := openMetaNcaDataSection(file, securePartitionOffset+int64(controlNca.StartOffset)) 66 | if err != nil { 67 | return nil, err 68 | } 69 | if fsHeader.fsType == 0 { 70 | romFsHeader, err := readRomfsHeader(section) 71 | if err != nil { 72 | return nil, err 73 | } 74 | fEntries, err := readRomfsFileEntry(section, romFsHeader) 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | if entry, ok := fEntries["control.nacp"]; ok { 80 | nacp, err := readNacp(section, romFsHeader, entry) 81 | if err != nil { 82 | return nil, err 83 | } 84 | return &nacp, nil 85 | } 86 | } else { 87 | return nil, errors.New("unsupported type " + control.ID) 88 | } 89 | } else { 90 | return nil, errors.New("unable to find control.nacp by id " + control.ID) 91 | } 92 | 93 | } 94 | return nil, errors.New("no control.nacp found") 95 | } 96 | 97 | /*https://switchbrew.org/wiki/NACP_Format*/ 98 | func readNacp(data []byte, romFsHeader RomfsHeader, fileEntry RomfsFileEntry) (Nacp, error) { 99 | offset := romFsHeader.DataOffset + fileEntry.offset 100 | titles := map[string]NacpTitle{} 101 | for i := 0; i < 16; i++ { 102 | //lang := i 103 | appTitleBytes := data[offset+(uint64(i)*0x300) : offset+(uint64(i)*0x300)+0x200] 104 | nameBytes := readBytesUntilZero(appTitleBytes) 105 | titles[Language(i).String()] = NacpTitle{Language: Language(i), Title: string(nameBytes)} 106 | } 107 | 108 | isbn := readBytesUntilZero(data[offset+0x3000 : offset+0x3000+0x25]) 109 | displayVersion := readBytesUntilZero(data[offset+0x3060 : offset+0x3060+0x10]) 110 | supportedLanguageFlag := binary.BigEndian.Uint32(data[offset+0x302C : offset+0x302C+0x4]) 111 | 112 | return Nacp{TitleName: titles, Isbn: string(isbn), DisplayVersion: string(displayVersion), SupportedLanguageFlag: supportedLanguageFlag}, nil 113 | /* 114 | 115 | 116 | Isbn = reader.ReadUtf8Z(37); 117 | reader.BaseStream.Position = start + 0x3025; 118 | StartupUserAccount = reader.ReadByte(); 119 | UserAccountSwitchLock = reader.ReadByte(); 120 | AocRegistrationType = reader.ReadByte(); 121 | AttributeFlag = reader.ReadInt32(); 122 | supportedLanguageFlag = reader.ReadUInt32(); 123 | ParentalControlFlag = reader.ReadUInt32(); 124 | Screenshot = reader.ReadByte(); 125 | VideoCapture = reader.ReadByte(); 126 | DataLossConfirmation = reader.ReadByte(); 127 | PlayLogPolicy = reader.ReadByte(); 128 | PresenceGroupId = reader.ReadUInt64(); 129 | 130 | for (int i = 0; i < RatingAge.Length; i++) 131 | { 132 | RatingAge[i] = reader.ReadSByte(); 133 | } 134 | 135 | DisplayVersion = reader.ReadUtf8Z(16); 136 | reader.BaseStream.Position = start + 0x3070; 137 | AddOnContentBaseId = reader.ReadUInt64(); 138 | SaveDataOwnerId = reader.ReadUInt64(); 139 | UserAccountSaveDataSize = reader.ReadInt64(); 140 | UserAccountSaveDataJournalSize = reader.ReadInt64(); 141 | DeviceSaveDataSize = reader.ReadInt64(); 142 | DeviceSaveDataJournalSize = reader.ReadInt64(); 143 | BcatDeliveryCacheStorageSize = reader.ReadInt64(); 144 | ApplicationErrorCodeCategory = reader.ReadUtf8Z(8); 145 | reader.BaseStream.Position = start + 0x30B0; 146 | 147 | for (int i = 0; i < LocalCommunicationId.Length; i++) 148 | { 149 | LocalCommunicationId[i] = reader.ReadUInt64(); 150 | } 151 | 152 | LogoType = reader.ReadByte(); 153 | LogoHandling = reader.ReadByte(); 154 | RuntimeAddOnContentInstall = reader.ReadByte(); 155 | Reserved00 = reader.ReadBytes(3); 156 | CrashReport = reader.ReadByte(); 157 | Hdcp = reader.ReadByte(); 158 | SeedForPseudoDeviceId = reader.ReadUInt64(); 159 | BcatPassphrase = reader.ReadUtf8Z(65); 160 | 161 | reader.BaseStream.Position = start + 0x3141; 162 | Reserved01 = reader.ReadByte(); 163 | Reserved02 = reader.ReadBytes(6); 164 | 165 | UserAccountSaveDataSizeMax = reader.ReadInt64(); 166 | UserAccountSaveDataJournalSizeMax = reader.ReadInt64(); 167 | DeviceSaveDataSizeMax = reader.ReadInt64(); 168 | DeviceSaveDataJournalSizeMax = reader.ReadInt64(); 169 | TemporaryStorageSize = reader.ReadInt64(); 170 | CacheStorageSize = reader.ReadInt64(); 171 | CacheStorageJournalSize = reader.ReadInt64(); 172 | CacheStorageDataAndJournalSizeMax = reader.ReadInt64(); 173 | CacheStorageIndex = reader.ReadInt16(); 174 | Reserved03 = reader.ReadBytes(6); 175 | 176 | for (int i = 0; i < 16; i++) 177 | { 178 | ulong value = reader.ReadUInt64(); 179 | if (value != 0) PlayLogQueryableApplicationId.Add(value); 180 | } 181 | 182 | PlayLogQueryCapability = reader.ReadByte(); 183 | RepairFlag = reader.ReadByte(); 184 | ProgramIndex = reader.ReadByte(); 185 | 186 | UserTotalSaveDataSize = UserAccountSaveDataSize + UserAccountSaveDataJournalSize; 187 | DeviceTotalSaveDataSize = DeviceSaveDataSize + DeviceSaveDataJournalSize; 188 | TotalSaveDataSize = UserTotalSaveDataSize + DeviceTotalSaveDataSize; 189 | */ 190 | } 191 | 192 | func readBytesUntilZero(appTitleBytes []byte) []byte { 193 | var nameBytes []byte 194 | for _, b := range appTitleBytes { 195 | if b == 0x0 { 196 | break 197 | } else { 198 | nameBytes = append(nameBytes, b) 199 | } 200 | } 201 | return nameBytes 202 | } 203 | -------------------------------------------------------------------------------- /switchfs/nca.go: -------------------------------------------------------------------------------- 1 | package switchfs 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "encoding/binary" 7 | "encoding/hex" 8 | "errors" 9 | "fmt" 10 | "github.com/giwty/switch-library-manager/settings" 11 | "github.com/giwty/switch-library-manager/switchfs/_crypto" 12 | "io" 13 | ) 14 | 15 | const ( 16 | NcaSectionType_Code = iota 17 | NcaSectionType_Data 18 | NcaSectionType_Logo 19 | ) 20 | const ( 21 | NcaContentType_Program = iota 22 | NcaContentType_Meta 23 | NcaContentType_Control 24 | NcaContentType_Manual 25 | NcaContentType_Data 26 | NcaContentType_PublicData 27 | ) 28 | 29 | func openMetaNcaDataSection(reader io.ReaderAt, ncaOffset int64) (*fsHeader, []byte, error) { 30 | //read the NCA headerBytes 31 | encNcaHeader := make([]byte, 0xC00) 32 | n, err := reader.ReadAt(encNcaHeader, ncaOffset) 33 | 34 | if err != nil { 35 | return nil, nil, errors.New("failed to read NCA header " + err.Error()) 36 | } 37 | if n != 0xC00 { 38 | return nil, nil, errors.New("failed to read NCA header") 39 | } 40 | 41 | keys, err := settings.SwitchKeys() 42 | if err != nil { 43 | return nil, nil, err 44 | } 45 | headerKey := keys.GetKey("header_key") 46 | if headerKey == "" { 47 | return nil, nil, errors.New("missing key - header_key") 48 | } 49 | ncaHeader, err := DecryptNcaHeader(headerKey, encNcaHeader) 50 | if err != nil { 51 | return nil, nil, err 52 | } 53 | 54 | if ncaHeader.HasRightsId() { 55 | //fail - need title keys 56 | return nil, nil, errors.New("non standard encryption is not supported") 57 | } 58 | 59 | /*if ncaHeader.contentType != NcaContentType_Meta { 60 | return nil, errors.New("not a meta NCA") 61 | }*/ 62 | 63 | dataSectionIndex := 0 64 | 65 | fsHeader, err := getFsHeader(ncaHeader, dataSectionIndex) 66 | if err != nil { 67 | return nil, nil, err 68 | } 69 | 70 | entry := getFsEntry(ncaHeader, dataSectionIndex) 71 | 72 | if entry.Size == 0 { 73 | return nil, nil, errors.New("empty section") 74 | } 75 | 76 | encodedEntryContent := make([]byte, entry.Size) 77 | entryOffset := ncaOffset + int64(entry.StartOffset) 78 | _, err = reader.ReadAt(encodedEntryContent, entryOffset) 79 | if err != nil { 80 | return nil, nil, err 81 | } 82 | if fsHeader.encType != 3 { 83 | return nil, nil, errors.New("non supported encryption type [encryption type:" + string(fsHeader.encType)) 84 | } 85 | 86 | /*if fsHeader.hashType != 2 { //Sha256 (FS_TYPE_PFS0) 87 | return nil, errors.New("non FS_TYPE_PFS0") 88 | }*/ 89 | decoded, err := decryptAesCtr(ncaHeader, fsHeader, entry.StartOffset, entry.Size, encodedEntryContent) 90 | if err != nil { 91 | return nil, nil, err 92 | } 93 | hashInfo, err := fsHeader.getHashInfo() 94 | if err != nil { 95 | return nil, nil, err 96 | } 97 | 98 | return fsHeader, decoded[hashInfo.pfs0HeaderOffset:], nil 99 | } 100 | 101 | func decryptAesCtr(ncaHeader *ncaHeader, fsHeader *fsHeader, offset uint32, size uint32, encoded []byte) ([]byte, error) { 102 | keyRevision := ncaHeader.getKeyRevision() 103 | cryptoType := ncaHeader.cryptoType 104 | 105 | if cryptoType != 0 { 106 | return []byte{}, errors.New("unsupported crypto type") 107 | } 108 | 109 | keys, _ := settings.SwitchKeys() 110 | 111 | keyName := fmt.Sprintf("key_area_key_application_0%x", keyRevision) 112 | KeyString := keys.GetKey(keyName) 113 | if KeyString == "" { 114 | return nil, errors.New(fmt.Sprintf("missing Key_area_key[%v]", keyName)) 115 | } 116 | key, _ := hex.DecodeString(KeyString) 117 | 118 | decKey := _crypto.DecryptAes128Ecb(ncaHeader.encryptedKeys[0x20:0x30], key) 119 | 120 | counter := make([]byte, 0x10) 121 | binary.BigEndian.PutUint64(counter, uint64(fsHeader.generation)) 122 | binary.BigEndian.PutUint64(counter[8:], uint64(offset/0x10)) 123 | 124 | c, _ := aes.NewCipher(decKey) 125 | 126 | decContent := make([]byte, size) 127 | 128 | s := cipher.NewCTR(c, counter) 129 | s.XORKeyStream(decContent, encoded[0:size]) 130 | 131 | return decContent, nil 132 | } 133 | -------------------------------------------------------------------------------- /switchfs/ncaHeader.go: -------------------------------------------------------------------------------- 1 | package switchfs 2 | 3 | import ( 4 | "crypto/aes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "github.com/giwty/switch-library-manager/switchfs/_crypto" 8 | "strconv" 9 | ) 10 | 11 | //https://switchbrew.org/wiki/NCA_Format 12 | 13 | type ncaHeader struct { 14 | headerBytes []byte 15 | rightsId []byte 16 | titleId []byte 17 | distribution byte 18 | contentType byte // (0x00 = Program, 0x01 = Meta, 0x02 = Control, 0x03 = Manual, 0x04 = Data, 0x05 = PublicData) 19 | keyGeneration2 byte 20 | keyGeneration1 byte 21 | encryptedKeys []byte // 4 * 0x10 22 | cryptoType byte //(0x00 = Application, 0x01 = Ocean, 0x02 = System) 23 | 24 | } 25 | 26 | func (n *ncaHeader) HasRightsId() bool { 27 | for i := 0; i < 0x10; i++ { 28 | if n.rightsId[i] != 0 { 29 | return true 30 | } 31 | } 32 | return false 33 | } 34 | 35 | func (n *ncaHeader) getKeyRevision() int { 36 | keyGeneration := max(n.keyGeneration1, n.keyGeneration2) 37 | keyRevision := keyGeneration - 1 38 | if keyGeneration == 0 { 39 | keyRevision = 0 40 | } 41 | return int(keyRevision) 42 | } 43 | 44 | func max(a byte, b byte) byte { 45 | if a > b { 46 | return a 47 | } 48 | return b 49 | } 50 | 51 | func DecryptNcaHeader(key string, encHeader []byte) (*ncaHeader, error) { 52 | headerKey, _ := hex.DecodeString(key) 53 | c, err := _crypto.NewCipher(aes.NewCipher, headerKey) 54 | if err != nil { 55 | return nil, err 56 | } 57 | sector := 0 58 | sectorSize := 0x200 59 | endOffset := 0x400 60 | decryptNcaHeader, err := _decryptNcaHeader(c, encHeader, endOffset, sectorSize, sector) 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | magic := string(decryptNcaHeader[0x200:0x204]) 66 | 67 | if magic == "NCA3" { 68 | endOffset = 0xC00 69 | decryptNcaHeader, err = _decryptNcaHeader(c, encHeader, endOffset, sectorSize, sector) 70 | } 71 | 72 | result := ncaHeader{headerBytes: decryptNcaHeader} 73 | 74 | result.distribution = decryptNcaHeader[0x204:0x205][0] 75 | result.contentType = decryptNcaHeader[0x205:0x206][0] 76 | result.rightsId = decryptNcaHeader[0x230 : 0x230+0x10] 77 | 78 | title_id_dec := binary.LittleEndian.Uint64(decryptNcaHeader[0x210 : 0x210+0x8]) 79 | result.titleId = []byte(strconv.FormatInt(int64(title_id_dec), 16)) 80 | result.keyGeneration1 = decryptNcaHeader[0x206:0x207][0] 81 | result.keyGeneration2 = decryptNcaHeader[0x220:0x221][0] 82 | 83 | encryptedKeysAreaOffset := 0x300 84 | result.encryptedKeys = decryptNcaHeader[encryptedKeysAreaOffset : encryptedKeysAreaOffset+(0x10*4)] 85 | 86 | result.cryptoType = decryptNcaHeader[0x207:0x208][0] 87 | 88 | return &result, nil 89 | } 90 | 91 | func _decryptNcaHeader(c *_crypto.Cipher, header []byte, end int, sectorSize int, sectorNum int) ([]byte, error) { 92 | decrypted := make([]byte, len(header)) 93 | for pos := 0; pos < end; pos += sectorSize { 94 | /* Workaround for Nintendo's custom sector...manually generate the tweak. */ 95 | tweak := getNintendoTweak(sectorNum) 96 | 97 | pos := sectorSize * sectorNum 98 | c.Decrypt(decrypted[pos:pos+sectorSize], header[pos:pos+sectorSize], &tweak) 99 | sectorNum++ 100 | } 101 | return decrypted, nil 102 | } 103 | 104 | func getNintendoTweak(sector int) [16]byte { 105 | tweak := [16]byte{} 106 | for i := 0xF; i >= 0; i-- { /* Nintendo LE custom tweak... */ 107 | tweak[i] = uint8(sector & 0xFF) 108 | sector >>= 8 109 | } 110 | return tweak 111 | } 112 | -------------------------------------------------------------------------------- /switchfs/nsp.go: -------------------------------------------------------------------------------- 1 | package switchfs 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "go.uber.org/zap" 7 | "strings" 8 | ) 9 | 10 | func ReadNspMetadata(filePath string) (map[string]*ContentMetaAttributes, error) { 11 | 12 | pfs0, err := ReadPfs0File(filePath) 13 | if err != nil { 14 | return nil, errors.New("Invalid NSP file, reason - [" + err.Error() + "]") 15 | } 16 | 17 | file, err := OpenFile(filePath) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | defer file.Close() 23 | 24 | contentMap := map[string]*ContentMetaAttributes{} 25 | 26 | for _, pfs0File := range pfs0.Files { 27 | 28 | fileOffset := int64(pfs0File.StartOffset) 29 | 30 | if strings.Contains(pfs0File.Name, "cnmt.nca") { 31 | _, section, err := openMetaNcaDataSection(file, fileOffset) 32 | if err != nil { 33 | return nil, err 34 | } 35 | currpfs0, err := readPfs0(bytes.NewReader(section), 0x0) 36 | if err != nil { 37 | return nil, err 38 | } 39 | currCnmt, err := readBinaryCnmt(currpfs0, section) 40 | if err != nil { 41 | return nil, err 42 | } 43 | if currCnmt.Type != "DLC" { 44 | nacp, err := ExtractNacp(currCnmt, file, pfs0, 0) 45 | if err != nil { 46 | zap.S().Debug("Failed to extract nacp [%v]\n", err.Error()) 47 | } 48 | currCnmt.Ncap = nacp 49 | } 50 | 51 | contentMap[currCnmt.TitleId] = currCnmt 52 | 53 | } /*else if strings.Contains(pfs0File.Name, ".cnmt.xml") { 54 | xmlBytes := make([]byte, pfs0File.Size) 55 | _, err = file.ReadAt(xmlBytes, fileOffset) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | currCnmt, err := readXmlCnmt(xmlBytes) 61 | if err != nil { 62 | return nil, err 63 | } 64 | contentMap[currCnmt.TitleId] = currCnmt 65 | }*/ 66 | } 67 | return contentMap, nil 68 | 69 | } 70 | -------------------------------------------------------------------------------- /switchfs/pfs0.go: -------------------------------------------------------------------------------- 1 | package switchfs 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "io" 7 | ) 8 | 9 | const ( 10 | PfsfileEntryTableSize = 0x18 11 | HfsfileEntryTableSize = 0x40 12 | pfs0Magic = "PFS0" 13 | hfs0Magic = "HFS0" 14 | ) 15 | 16 | type fileEntry struct { 17 | StartOffset uint64 18 | Size uint64 19 | Name string 20 | } 21 | 22 | // PFS0 struct to represent PFS0 filesystem of NSP 23 | type PFS0 struct { 24 | Filepath string 25 | Size uint64 26 | HeaderLen uint16 27 | Files []fileEntry 28 | } 29 | 30 | // https://wiki.oatmealdome.me/PFS0_(File_Format) 31 | func ReadPfs0File(filePath string) (*PFS0, error) { 32 | 33 | file, err := OpenFile(filePath) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | defer file.Close() 39 | 40 | p, err := readPfs0(file, 0x0) 41 | if err != nil { 42 | return nil, err 43 | } 44 | p.Filepath = filePath 45 | return p, nil 46 | } 47 | 48 | func readPfs0(reader io.ReaderAt, offset int64) (*PFS0, error) { 49 | 50 | header := make([]byte, 0xC) 51 | n, err := reader.ReadAt(header, offset) 52 | if err != nil { 53 | return nil, err 54 | } 55 | if n != 0xC { 56 | return nil, errors.New("failed to read file") 57 | } 58 | var fileEntryTableSize uint16 59 | if string(header[:0x4]) == pfs0Magic { 60 | fileEntryTableSize = PfsfileEntryTableSize 61 | } else if string(header[:0x4]) == hfs0Magic { 62 | fileEntryTableSize = HfsfileEntryTableSize 63 | } else { 64 | return nil, errors.New("Invalid NSP headerBytes. Expected 'PFS0'/'HFS0', got '" + string(header[:0x4]) + "'") 65 | } 66 | p := &PFS0{} 67 | 68 | fileCount := binary.LittleEndian.Uint16(header[0x4:0x8]) 69 | 70 | fileEntryTableOffset := 0x10 + (fileEntryTableSize * fileCount) 71 | 72 | stringsLen := binary.LittleEndian.Uint16(header[0x8:0xC]) 73 | p.HeaderLen = fileEntryTableOffset + stringsLen 74 | fileNamesBuffer := make([]byte, stringsLen) 75 | _, err = reader.ReadAt(fileNamesBuffer, offset+int64(fileEntryTableOffset)) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | p.Files = make([]fileEntry, fileCount) 81 | // go over the fileEntries 82 | for i := uint16(0); i < fileCount; i++ { 83 | fileEntryTable := make([]byte, fileEntryTableSize) 84 | _, err = reader.ReadAt(fileEntryTable, offset+int64(0x10+(fileEntryTableSize*i))) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | fileOffset := binary.LittleEndian.Uint64(fileEntryTable[0:8]) 90 | fileSize := binary.LittleEndian.Uint64(fileEntryTable[8:16]) 91 | var nameBytes []byte 92 | for _, b := range fileNamesBuffer[binary.LittleEndian.Uint16(fileEntryTable[16:20]):] { 93 | if b == 0x0 { 94 | break 95 | } else { 96 | nameBytes = append(nameBytes, b) 97 | } 98 | } 99 | 100 | p.Files[i] = fileEntry{fileOffset + uint64(p.HeaderLen), fileSize, string(nameBytes)} 101 | } 102 | p.HeaderLen += stringsLen 103 | 104 | return p, nil 105 | } 106 | -------------------------------------------------------------------------------- /switchfs/romfs.go: -------------------------------------------------------------------------------- 1 | package switchfs 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | ) 7 | 8 | type RomfsHeader struct { 9 | HeaderSize uint64 10 | DirHashTableOffset uint64 11 | DirHashTableSize uint64 12 | DirMetaTableOffset uint64 13 | DirMetaTableSize uint64 14 | FileHashTableOffset uint64 15 | FileHashTableSize uint64 16 | FileMetaTableOffset uint64 17 | FileMetaTableSize uint64 18 | DataOffset uint64 19 | } 20 | 21 | type RomfsFileEntry struct { 22 | parent uint32 23 | sibling uint32 24 | offset uint64 25 | size uint64 26 | hash uint32 27 | name_size uint32 28 | name string 29 | } 30 | 31 | func readRomfsHeader(data []byte) (RomfsHeader, error) { 32 | header := RomfsHeader{} 33 | header.HeaderSize = binary.LittleEndian.Uint64(data[0x0+(0x8*0) : 0x0+(0x8*1)]) 34 | header.DirHashTableOffset = binary.LittleEndian.Uint64(data[0x0+(0x8*1) : 0x0+(0x8*2)]) 35 | header.DirHashTableSize = binary.LittleEndian.Uint64(data[0x0+(0x8*2) : 0x0+(0x8*3)]) 36 | header.DirMetaTableOffset = binary.LittleEndian.Uint64(data[0x0+(0x8*3) : 0x0+(0x8*4)]) 37 | header.DirMetaTableSize = binary.LittleEndian.Uint64(data[0x0+(0x8*4) : 0x0+(0x8*5)]) 38 | header.FileHashTableOffset = binary.LittleEndian.Uint64(data[0x0+(0x8*5) : 0x0+(0x8*6)]) 39 | header.FileHashTableSize = binary.LittleEndian.Uint64(data[0x0+(0x8*6) : 0x0+(0x8*7)]) 40 | header.FileMetaTableOffset = binary.LittleEndian.Uint64(data[0x0+(0x8*7) : 0x0+(0x8*8)]) 41 | header.FileMetaTableSize = binary.LittleEndian.Uint64(data[0x0+(0x8*8) : 0x0+(0x8*9)]) 42 | header.DataOffset = binary.LittleEndian.Uint64(data[0x0+(0x8*9) : 0x0+(0x8*10)]) 43 | return header, nil 44 | } 45 | 46 | func readRomfsFileEntry(data []byte, header RomfsHeader) (map[string]RomfsFileEntry, error) { 47 | if header.FileMetaTableOffset+header.FileMetaTableSize > uint64(len(data)) { 48 | return nil, errors.New("failed to read romfs") 49 | } 50 | dirBytes := data[header.FileMetaTableOffset : header.FileMetaTableOffset+header.FileMetaTableSize] 51 | result := map[string]RomfsFileEntry{} 52 | offset := uint32(0x0) 53 | for offset < uint32(header.FileHashTableSize) { 54 | entry := RomfsFileEntry{} 55 | entry.parent = binary.LittleEndian.Uint32(dirBytes[offset : offset+0x4]) 56 | entry.sibling = binary.LittleEndian.Uint32(dirBytes[offset+0x4 : offset+0x8]) 57 | entry.offset = binary.LittleEndian.Uint64(dirBytes[offset+0x8 : offset+0x10]) 58 | entry.size = binary.LittleEndian.Uint64(dirBytes[offset+0x10 : offset+0x18]) 59 | entry.hash = binary.LittleEndian.Uint32(dirBytes[offset+0x18 : offset+0x1C]) 60 | entry.name_size = binary.LittleEndian.Uint32(dirBytes[offset+0x1C : offset+0x20]) 61 | entry.name = string(dirBytes[offset+0x20 : (offset+0x20)+entry.name_size]) 62 | result[entry.name] = entry 63 | offset = offset + 0x20 + entry.name_size 64 | } 65 | return result, nil 66 | 67 | //fmt.Println(string(section[DataOffset+offset+0x3060:DataOffset+offset+0x3060 +0x10])) 68 | } 69 | -------------------------------------------------------------------------------- /switchfs/splitFileReader.go: -------------------------------------------------------------------------------- 1 | package switchfs 2 | 3 | import ( 4 | "errors" 5 | "github.com/avast/retry-go" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "path" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | type ReadAtCloser interface { 15 | io.ReaderAt 16 | io.Closer 17 | } 18 | 19 | type splitFile struct { 20 | info []os.FileInfo 21 | files []ReadAtCloser 22 | path string 23 | chunkSize int64 24 | } 25 | 26 | type fileWrapper struct { 27 | file ReadAtCloser 28 | path string 29 | } 30 | 31 | func NewFileWrapper(filePath string) (*fileWrapper, error) { 32 | result := fileWrapper{} 33 | result.path = filePath 34 | file, err := _openFile(filePath) 35 | if err != nil { 36 | return nil, err 37 | } 38 | result.file = file 39 | return &result, nil 40 | } 41 | 42 | func (sp *fileWrapper) ReadAt(p []byte, off int64) (n int, err error) { 43 | if sp.file != nil { 44 | return sp.file.ReadAt(p, off) 45 | } 46 | return 0, errors.New("file is not opened") 47 | } 48 | 49 | func (sp *fileWrapper) Close() error { 50 | 51 | if sp.file != nil { 52 | return sp.file.Close() 53 | } 54 | 55 | return nil 56 | } 57 | 58 | func NewSplitFileReader(filePath string) (*splitFile, error) { 59 | result := splitFile{} 60 | index := strings.LastIndex(filePath, string(os.PathSeparator)) 61 | splitFileFolder := filePath[:index] 62 | files, err := ioutil.ReadDir(splitFileFolder) 63 | if err != nil { 64 | return nil, err 65 | } 66 | result.path = splitFileFolder 67 | result.chunkSize = files[0].Size() 68 | result.info = make([]os.FileInfo, 0, len(files)) 69 | result.files = make([]ReadAtCloser, len(files)) 70 | for _, file := range files { 71 | if _, err := strconv.Atoi(file.Name()[len(file.Name())-1:]); err == nil { 72 | result.info = append(result.info, file) 73 | } 74 | } 75 | return &result, nil 76 | } 77 | 78 | func (sp *splitFile) ReadAt(p []byte, off int64) (n int, err error) { 79 | //calculate the part containing the offset 80 | part := int(off / sp.chunkSize) 81 | 82 | if len(sp.info) < part { 83 | return 0, errors.New("missing part " + strconv.Itoa(part)) 84 | } 85 | 86 | if len(sp.files) == 0 || sp.files[part] == nil { 87 | file, _ := _openFile(path.Join(sp.path, sp.info[part].Name())) 88 | sp.files[part] = file 89 | } 90 | off = off - sp.chunkSize*int64(part) 91 | 92 | if off < 0 || off > sp.info[part].Size() { 93 | return 0, errors.New("offset is out of bounds") 94 | } 95 | return sp.files[part].ReadAt(p, off) 96 | } 97 | 98 | func _openFile(path string) (*os.File, error) { 99 | var file *os.File 100 | var err error 101 | retry.Attempts(5) 102 | err = retry.Do( 103 | func() error { 104 | file, err = os.Open(path) 105 | return err 106 | }, 107 | ) 108 | return file, err 109 | } 110 | 111 | func (sp *splitFile) Close() error { 112 | for _, file := range sp.files { 113 | if file != nil { 114 | file.Close() 115 | } 116 | } 117 | return nil 118 | } 119 | 120 | func OpenFile(filePath string) (ReadAtCloser, error) { 121 | //check if it's a split file 122 | if _, err := strconv.Atoi(filePath[len(filePath)-1:]); err == nil { 123 | return NewSplitFileReader(filePath) 124 | } else { 125 | return NewFileWrapper(filePath) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /switchfs/xci.go: -------------------------------------------------------------------------------- 1 | package switchfs 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "go.uber.org/zap" 8 | "io" 9 | "strings" 10 | ) 11 | 12 | func ReadXciMetadata(filePath string) (map[string]*ContentMetaAttributes, error) { 13 | file, err := OpenFile(filePath) 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | defer file.Close() 19 | 20 | header := make([]byte, 0x200) 21 | _, err = file.ReadAt(header, 0) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | if string(header[0x100:0x104]) != "HEAD" { 27 | return nil, errors.New("Invalid XCI headerBytes. Expected 'HEAD', got '" + string(header[:0x4]) + "'") 28 | } 29 | 30 | rootPartitionOffset := binary.LittleEndian.Uint64(header[0x130:0x138]) 31 | //rootPartitionSize := binary.LittleEndian.Uint64(header[0x138:0x140]) 32 | 33 | rootHfs0, err := readPfs0(file, int64(rootPartitionOffset)) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | secureHfs0, secureOffset, err := readSecurePartition(file, rootHfs0, rootPartitionOffset) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | contentMap := map[string]*ContentMetaAttributes{} 44 | 45 | for _, pfs0File := range secureHfs0.Files { 46 | 47 | fileOffset := secureOffset + int64(pfs0File.StartOffset) 48 | 49 | if strings.Contains(pfs0File.Name, "cnmt.nca") { 50 | _, section, err := openMetaNcaDataSection(file, fileOffset) 51 | if err != nil { 52 | return nil, err 53 | } 54 | currPfs0, err := readPfs0(bytes.NewReader(section), 0x0) 55 | if err != nil { 56 | return nil, err 57 | } 58 | currCnmt, err := readBinaryCnmt(currPfs0, section) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | if currCnmt.Type == "BASE" || currCnmt.Type == "UPD" { 64 | nacp, err := ExtractNacp(currCnmt, file, secureHfs0, secureOffset) 65 | if err != nil { 66 | zap.S().Debug("Failed to extract nacp [%v]\n", err.Error()) 67 | } 68 | currCnmt.Ncap = nacp 69 | } 70 | 71 | contentMap[currCnmt.TitleId] = currCnmt 72 | 73 | } /* else if strings.Contains(pfs0File.Name, ".cnmt.xml") { 74 | xmlBytes := make([]byte, pfs0File.Size) 75 | _, err = file.ReadAt(xmlBytes, fileOffset) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | currCnmt, err := readXmlCnmt(xmlBytes) 81 | if err != nil { 82 | return nil, err 83 | } 84 | contentMap[currCnmt.TitleId] = currCnmt 85 | }*/ 86 | } 87 | 88 | return contentMap, nil 89 | } 90 | 91 | func getNcaById(hfs0 *PFS0, id string) *fileEntry { 92 | for _, fileEntry := range hfs0.Files { 93 | if strings.Contains(fileEntry.Name, id) { 94 | return &fileEntry 95 | } 96 | } 97 | return nil 98 | } 99 | 100 | func readSecurePartition(file io.ReaderAt, hfs0 *PFS0, rootPartitionOffset uint64) (*PFS0, int64, error) { 101 | for _, hfs0File := range hfs0.Files { 102 | offset := int64(rootPartitionOffset) + int64(hfs0File.StartOffset) 103 | 104 | if hfs0File.Name == "secure" { 105 | securePartition, err := readPfs0(file, offset) 106 | if err != nil { 107 | return nil, 0, err 108 | } 109 | return securePartition, offset, nil 110 | } 111 | } 112 | return nil, 0, nil 113 | } 114 | -------------------------------------------------------------------------------- /updates_ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giwty/switch-library-manager/f69daa7e5e3bc9fc2cc17ad839a12ac3bf5e0372/updates_ui.png --------------------------------------------------------------------------------