diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f3cf888..889f81a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,34 +1,99 @@ -name: Build - +name: "Build" on: + workflow_dispatch: push: branches: - - main - development + - main pull_request: branches: - - main - development + - main + +permissions: + contents: write jobs: - build: + package: + name: Package strategy: - fail-fast: false matrix: - build: - [ - { - name: desktop-manager, - platform: windows/amd64, - os: windows-latest, - }, - ] - runs-on: ${{ matrix.build.os }} + platform: [windows-latest] + build-name: ["iconium"] + go-version: [1.22] + node-version: [20] + runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Setup Wails + run: go install github.com/wailsapp/wails/v2/cmd/wails@latest + + - name: Build Wails app + run: wails build -nsis + + - name: Sign Windows binaries + shell: powershell + run: | + echo "Creating certificate file" + New-Item -ItemType directory -Path certificate + Set-Content -Path certificate\certificate.txt -Value '${{ secrets.WIN_SIGNING_CERT }}' + certutil -decode certificate\certificate.txt certificate\certificate.pfx + echo "Signing Binary" + & 'C:/Program Files (x86)/Windows Kits/10/bin/10.0.17763.0/x86/signtool.exe' sign /fd sha256 /tr http://ts.ssl.com /f certificate\certificate.pfx /p '${{ secrets.WIN_SIGNING_CERT_PASSWORD }}' .\build\bin\${{matrix.build-name}}.exe + echo "Signing Installer" + & 'C:/Program Files (x86)/Windows Kits/10/bin/10.0.17763.0/x86/signtool.exe' sign /fd sha256 /tr http://ts.ssl.com /f certificate\certificate.pfx /p '${{ secrets.WIN_SIGNING_CERT_PASSWORD }}' .\build\bin\${{matrix.build-name}}-amd64-installer.exe + + - name: Upload artifacts + uses: actions/upload-artifact@v4 with: - submodules: recursive - - uses: dAppServer/wails-build-action@v2.2 + name: binaries + path: build/bin/* + + extract-version: + if: github.ref == 'refs/heads/main' + name: Extract version + runs-on: ubuntu-latest + + outputs: + version: ${{ steps.extract_version.outputs.version }} + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Extract version + id: extract_version + run: | + version=$(jq -r '.info.productVersion' wails.json) + echo "version=$version" >> $GITHUB_OUTPUT + + create-release: + if: github.ref == 'refs/heads/main' + name: Create release + needs: [extract-version, package] + runs-on: ubuntu-latest + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 with: - build-name: ${{ matrix.build.name }} - build-platform: ${{ matrix.build.platform }} + name: binaries + path: ./artifacts + + - name: Create Draft Release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + run: | + gh release create v${{ needs.extract-version.outputs.version }} ./artifacts/* --title "Release v${{ needs.extract-version.outputs.version }}" --draft --generate-notes \ No newline at end of file diff --git a/.gitignore b/.gitignore index 58f282c..f7929bc 100644 --- a/.gitignore +++ b/.gitignore @@ -7,11 +7,6 @@ yarn-error.log* pnpm-debug.log* lerna-debug.log* -node_modules -dist -dist-ssr -*.local - # Editor directories and files .vscode/* !.vscode/extensions.json @@ -23,6 +18,17 @@ dist-ssr *.sln *.sw? -# wails +# Wails *.exe~ -./build/bin/* \ No newline at end of file +*.syso +*.local +node_modules +/build/bin +/build/windows/installer/wails_tools.nsh +/build/windows/installer/tmp +/frontend/dist +/frontend/dist-ssr +/frontend/package.json.md5 + +# Certificate files +certificate.* diff --git a/README.md b/README.md index 98b0d2e..c2b88c2 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,77 @@ -# Desktop Manager -Icon and description editor, profile creation tool for your desktop. (WIP) +# Iconium -## Screenshots +

+ + Logo + +
+ Website -![Main](./assets/screenshot-1.png) +**Iconium** is a flexible tool for creating and managing icon packs. Iconium can apply icons to `.lnk`, `.url` and directories and extract them from files such as `.png`, `.jpg`, `.jpeg`, `.webp`, `.svg`, `.bmp`, `.ico`, `.exe`, `.lnk` and `.url` files. It also provides advanced features like file matching with environment variables or wildcards and customizing icon radius and opacity. -## Features +## Table of Contents +- [Iconium](#iconium) + - [Table of Contents](#table-of-contents) + - [Screenshots](#screenshots) + - [Features](#features) + - [Planned Features](#planned-features) + - [Installation](#installation) + - [Translation](#translation) + - [Technologies](#technologies) + - [License](#license) -- Create desktop profiles -- Edit icon and description -- Shortcut (.lnk) support +## Screenshots +![Screenshot 1](./assets/screenshot-1.png) +![Screenshot 2](./assets/screenshot-2.png) +![Screenshot 3](./assets/screenshot-3.png) -## Planned +## Features +- Create, distribute and use icon packs +- Edit icon radius and opacity +- Match files by environment variables, wildcards, or destination paths +- Supports `.lnk`, `.url`, and directories +- Can extract icons from `.png`, `.jpg`, `.jpeg`, `.webp`, `.svg`, `.bmp`, `.ico`, `.exe`, `.lnk` and `.url` files +- Highly customizable appearance +- Automatic update +- Color schemes and light/dark mode for each scheme -- Save desktop layout to profile -- Share profiles as files -- Folder and .url support -- Sync desktop with profile +## Planned Features +- Save desktop layouts to icon packs +- Auto-apply scheduling for icon packs +- Custom icon masks -## Built With +## Installation +1. Install Wails: [Wails installation guide](https://wails.io/docs/gettingstarted/installation). + +2. Clone the repository: + ```bash + git clone https://github.com/beyenilmez/iconium.git + ``` +3. Navigate to the project directory: + ```bash + cd iconium + ``` +4. Run in dev mode: + ```bash + wails dev + ``` + or + + Build: + ```bash + wails build + ``` +## Translation +1. Create a copy of `frontend/public/locales/en-US.json` file and rename it using your language code, which you can find [here](https://learn.microsoft.com/en-us/openspecs/office_standards/ms-oe376/6c085406-a698-4e12-9d4d-c3b0ee3dbc4a). +2. Translate the file to your language. +3. Update `frontend/src/locales.json` to include your language. -- [Wails](https://wails.io/) -- [React](https://react.dev/) -- [tailwindcss](https://tailwindcss.com/) -- [shadcn-ui](https://ui.shadcn.com/) -- [lucide-react](https://lucide.dev/guide/packages/lucide-react) +## Technologies +- **Backend**: [Go](https://go.dev/), [Wails](https://wails.io/) +- **Frontend**: [React](https://react.dev/), [TailwindCSS](https://tailwindcss.com/), [shadcnUI](https://ui.shadcn.com/) +- **Icon Processing**: [ImageMagick](https://imagemagick.org/), [ExtractIcon](https://github.com/bertjohnson/ExtractIcon) +- **Translation**: [i18next](https://react.i18next.com/) ## License - -Distributed under the MIT License. See [LICENSE](https://github.com/beyenilmez/desktop-manager/blob/main/LICENSE) for more information. +Distributed under the MIT License. See [LICENSE](https://github.com/beyenilmez/iconium/blob/main/LICENSE) for more information. diff --git a/app.go b/app.go index 1a7e025..0ad60cd 100644 --- a/app.go +++ b/app.go @@ -2,616 +2,318 @@ package main import ( "context" - _ "embed" - "encoding/base64" "encoding/json" "fmt" - "io" - "io/fs" - "net/http" "os" "os/exec" - "os/user" - "path" - "path/filepath" "strings" + "github.com/gen2brain/beeep" "github.com/google/uuid" - lnk "github.com/parsiya/golnk" - runtime "github.com/wailsapp/wails/v2/pkg/runtime" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/runtime" + "golang.org/x/sys/windows" ) -//go:embed frontend/public/setlnkicon.vbs -var setlnkicon string - -//go:embed frontend/public/setlnkdesc.vbs -var setlnkdesc string - // App struct type App struct { ctx context.Context } +var appContext context.Context +var app *App + // NewApp creates a new App application struct func NewApp() *App { - return &App{} + app = &App{} + return app } // startup is called at application startup func (a *App) startup(ctx context.Context) { - // Perform your setup here a.ctx = ctx -} - -// domReady is called after front-end resources have been loaded -func (a App) domReady(ctx context.Context) { - // Add your action here -} - -// beforeClose is called when the application is about to quit, -// either by clicking the window close button or calling runtime.Quit. -// Returning true will cause the application to continue, false will continue shutdown as normal. -func (a *App) beforeClose(ctx context.Context) (prevent bool) { - return false -} - -// shutdown is called at application termination -func (a *App) shutdown(ctx context.Context) { - // Perform your teardown here -} - -type fileInfo struct { - Name string `json:"name"` - Description string `json:"description"` - Path string `json:"path"` - Destination string `json:"destination"` - IconDestination string `json:"iconDestination"` - IconIndex int `json:"iconIndex"` - Extension string `json:"extension"` - IsFolder bool `json:"isFolder"` - IconId string `json:"iconId"` - IconName string `json:"iconName"` -} - -type profileInfo struct { - Value any `json:"value"` - Label string `json:"label"` -} - -type profile struct { - Name string `json:"name"` - Id string `json:"id"` - Value []fileInfo `json:"value"` -} + appContext = ctx -// Allowed file types -var allowedTypes = []string{".lnk"} + runtime.LogInfo(appContext, "Starting application") -func CheckErr(err error, msg string, fatal bool) { - if err != nil { - fmt.Printf("%s - %v\n", msg, err) - if fatal { - panic(msg) - } + // Set window position + if *config.WindowStartPositionX >= 0 && *config.WindowStartPositionY >= 0 { + runtime.LogInfo(appContext, "Setting window position") + runtime.WindowSetPosition(appContext, *config.WindowStartPositionX, *config.WindowStartPositionY) } -} -// Check if an item is in a slice -func contains(slice []string, item string) bool { - for _, value := range slice { - if value == item { - return true - } + // Set window size + if *config.WindowStartSizeX >= 0 && *config.WindowStartSizeY >= 0 && runtime.WindowIsNormal(appContext) { + runtime.LogInfo(appContext, "Setting window size") + runtime.WindowSetSize(appContext, *config.WindowStartSizeX, *config.WindowStartSizeY) } - return false -} -// Copy copies the contents of the file at srcpath to a regular file -// at dstpath. If the file named by dstpath already exists, it is -// truncated. The function does not copy the file mode, file -// permission bits, or file attributes. -// Source: https://stackoverflow.com/a/74107689 -func Copy(srcpath, dstpath string) (err error) { - r, err := os.Open(srcpath) - if err != nil { - return err - } - defer r.Close() // ignore error: file was opened read-only. + // Initiate paths + runtime.LogInfo(appContext, "Initiating paths") + err := path_init() - w, err := os.Create(dstpath) if err != nil { - return err + runtime.LogError(appContext, err.Error()) } - defer func() { - // Report the error, if any, from Close, but do so - // only if there isn't already an outgoing error. - if c := w.Close(); err == nil { - err = c - } - }() - - _, err = io.Copy(w, r) - return err -} - -// Returns the save directory -func getSaveDir() string { - userConfigDir, err := os.UserConfigDir() - - CheckErr(err, "Failed to get user config dir", true) + // Delete old log files + runtime.LogInfo(appContext, "Deleting old log files") + delete_old_logs() - return filepath.Join(userConfigDir, "desktop-manager") -} - -// Returns the profile directory -func getProfileDir() string { - return filepath.Join(getSaveDir(), "profiles") -} - -// Returns the icon directory -func getIconDir(profileName string) string { - return filepath.Join(getSaveDir(), "icon", profileName) -} - -// Returns the base64 icon directory -func getBase64Dir(profileName string) string { - return filepath.Join(getIconDir(profileName), "base64") + // Check if configPath exists + if _, err := os.Stat(configPath); os.IsNotExist(err) { + onFirstRun() + } } -// Returns the script directory -func getScriptDir() string { - return filepath.Join(getSaveDir(), "scripts") -} +// domReady is called after front-end resources have been loaded +func (a *App) domReady(ctx context.Context) { + // Get version from wails.json + var wailsDeccodedJSON map[string]interface{} + err := json.Unmarshal(wailsJSON, &wailsDeccodedJSON) + if err != nil { + runtime.LogError(appContext, "Failed to decode wails.json: "+err.Error()) + } + version = wailsDeccodedJSON["info"].(map[string]interface{})["productVersion"].(string) -// Returns the desktop paths -func getDesktopPaths() []string { - userDir, err := user.Current() - CheckErr(err, "Failed to get user dir", true) + // Check if admin privileges are needed + NeedsAdminPrivileges = checkAdminPrivileges() - homedir := userDir.HomeDir - desktop := filepath.Join(homedir, "Desktop") + // Get launch args + args = os.Args[1:] + runtime.LogInfo(appContext, "Launch args: "+strings.Join(args, " ")) - public := "C:\\Users\\Public\\Desktop" + // Show window + runtime.WindowShow(appContext) + runtime.Show(appContext) - return []string{desktop, public} -} + // Check updates + if *config.CheckForUpdates { + updateInfo := a.CheckForUpdate() -// GetFileInfo retrieves information about a file. -// -// Parameters: -// - path: the path of the directory containing the file. -// - file: the file to retrieve information about. -// -// Returns: -// - fileInfo: the information about the file. -// - error: an error if the file type is not allowed or if there was a failure reading the file. -func GetFileInfo(path string, file *fs.DirEntry) (fileInfo, error) { - fileName := (*file).Name() - extension := filepath.Ext(fileName) - noExtFileName := strings.TrimSuffix(fileName, extension) - - if !contains(allowedTypes, extension) { - return fileInfo{}, fmt.Errorf("file type not allowed") - } else if extension == ".lnk" { - filePath := filepath.Join(path, fileName) - - lnkInfo := fileInfo{ - Name: noExtFileName, - Description: "", - Path: filePath, - Destination: "", - IconDestination: "", - IconIndex: 0, - Extension: extension, - IsFolder: false, - IconId: "", - IconName: "", + if updateInfo.UpdateAvailable { + a.SendNotification("settings.setting.update.update_available", "v"+updateInfo.CurrentVersion+" ⭢ "+updateInfo.LatestVersion, "__settings__update", "info") } + } - f, err := lnk.File(filePath) - CheckErr(err, "Failed to read lnk file", false) + // Install external programs + restore_missing_external_programs() - if f.StringData.NameString != "" { - lnkInfo.Description = f.StringData.NameString + for i := 0; i < len(args); i++ { + switch args[i] { + case "--goto": + if i+1 < len(args) { + runtime.LogInfo(a.ctx, fmt.Sprintf("Goto: %s", args[i+1])) + runtime.WindowExecJS(a.ctx, fmt.Sprintf(`window.goto("%s");`, args[i+1])) + i++ + } + case "--notify": + if i+4 < len(args) { + runtime.LogInfo(a.ctx, "Notify: "+args[i+1]+" "+args[i+2]+" "+args[i+3]+" "+args[i+4]) + a.SendNotification(args[i+1], args[i+2], args[i+3], args[i+4]) + i += 4 + } + default: + runtime.LogInfo(a.ctx, fmt.Sprintf("Pack path: %s", args[i])) + runtime.WindowExecJS(a.ctx, "window.importIconPack('"+strings.ReplaceAll(args[i], "\\", "\\\\")+"')") } + } +} - if f.LinkInfo.LocalBasePath != "" { - lnkInfo.Destination = f.LinkInfo.LocalBasePath - } - if f.LinkInfo.LocalBasePathUnicode != "" { - lnkInfo.Destination = f.LinkInfo.LocalBasePathUnicode +// beforeClose is called when the application is about to quit, +// either by clicking the window close button or calling runtime.Quit. +// Returning true will cause the application to continue, false will continue shutdown as normal. +func (a *App) beforeClose(ctx context.Context) (prevent bool) { + if *config.SaveWindowStatus { + if runtime.WindowIsMaximised(a.ctx) { + var windowState = 2 + config.WindowStartState = &windowState + runtime.LogInfo(a.ctx, "Setting window state to maximized") + } else { + var windowState = 0 + config.WindowStartState = &windowState + runtime.LogInfo(a.ctx, "Setting window state to normal") } - if f.StringData.IconLocation != "" { - lnkInfo.IconDestination = f.StringData.IconLocation + windowPositionX, windowPositionY := runtime.WindowGetPosition(a.ctx) + if windowPositionX < 0 { + windowPositionX = 0 } - - if f.Header.IconIndex != 0 { - lnkInfo.IconIndex = int(f.Header.IconIndex) + if windowPositionY < 0 { + windowPositionY = 0 } + config.WindowStartPositionX, config.WindowStartPositionY = &windowPositionX, &windowPositionY + runtime.LogInfo(a.ctx, fmt.Sprintf("Setting window position to %d,%d", windowPositionX, windowPositionY)) - return lnkInfo, nil - } else { - return fileInfo{}, nil + windowSizeX, windowSizeY := runtime.WindowGetSize(a.ctx) + config.WindowStartSizeX, config.WindowStartSizeY = &windowSizeX, &windowSizeY + runtime.LogInfo(a.ctx, fmt.Sprintf("Setting window size to %d,%d", windowSizeX, windowSizeY)) } -} -// GetFileInfoSlice retrieves file information for the provided paths. -// -// paths: a slice of strings representing the directories to read files from. -// []fileInfo: a slice of fileInfo structs containing information about the files. -func GetFileInfoSlice(paths []string) []fileInfo { - icons := []fileInfo{} + runtime.LogInfo(a.ctx, "Saving config") + err := WriteConfig(configPath) - for _, path := range paths { - currentFiles, pathError := os.ReadDir(path) - CheckErr(pathError, "Failed to read directory", false) - - for _, file := range currentFiles { - fileInfo, err := GetFileInfo(path, &file) - CheckErr(err, "Failed to get file info", false) - - if fileInfo.Path != "" && contains(allowedTypes, fileInfo.Extension) { - icons = append(icons, fileInfo) - } - } - } - - return icons -} - -func (a *App) AddProfile(name string) { - // Create profile - profile := profile{ - Name: name, - Id: uuid.New().String(), - Value: []fileInfo{}, + if err != nil { + runtime.LogError(a.ctx, err.Error()) + return false } - // Convert to JSON - profileJSON, err := json.Marshal(profile) - CheckErr(err, "Failed to marshal profile", false) - - // Get save directory - profileDir := getProfileDir() + runtime.LogInfo(a.ctx, "Saving config complete") - // Create folder - err = os.MkdirAll(profileDir, os.ModePerm) - CheckErr(err, "Failed to create profile folder", false) - - // Write json - err = os.WriteFile(filepath.Join(profileDir, name), profileJSON, 0644) - CheckErr(err, "Failed to write profile file", false) -} - -func (a *App) RemoveProfile(profileName string) { - profileDir := getProfileDir() - err := os.Remove(filepath.Join(profileDir, profileName)) - CheckErr(err, "Failed to remove profile file", false) + return false } -func CopyIcons(profile *profile) { - for i, fileInfo := range (*profile).Value { - if filepath.Ext(fileInfo.IconDestination) == ".ico" { - // Generate UUID and save path - uuid := uuid.New().String() - savePath := filepath.Join(getIconDir((*profile).Name), uuid+".ico") - - // Create dir - err := os.MkdirAll(filepath.Dir(savePath), os.ModePerm) - CheckErr(err, "Failed to create icon folder", false) - - // Copy icon - err = Copy(fileInfo.IconDestination, savePath) - CheckErr(err, "Failed to copy icon", false) - - // Generate base64 version - GenerateBase64Icon(&(*profile).Name, &fileInfo.IconDestination, &uuid) - - // Icon name - fileInfo.IconName = uuid - - (*profile).Value[i] = fileInfo - } - } +// shutdown is called at application termination +func (a *App) shutdown(ctx context.Context) { + // Perform your teardown here } -func GenerateBase64Icon(profileName *string, filePath *string, fileName *string) { - // Create folder - err := os.MkdirAll(getBase64Dir(*profileName), os.ModePerm) - CheckErr(err, "Failed to create base64 image folder", false) +// onSecondInstanceLaunch is called when the application is launched from a second instance +func (a *App) onSecondInstanceLaunch(secondInstanceData options.SecondInstanceData) { + secondInstanceArgs := secondInstanceData.Args - destination := filepath.Join(getBase64Dir(*profileName), *fileName) - - // Read the entire file into a byte slice - bytes, err := os.ReadFile(*filePath) + file, err := os.Create(activeIconFolder + "a.txt") if err != nil { - fmt.Println(err) - } - - var base64Encoding string - - // Determine the content type of the image file - mimeType := http.DetectContentType(bytes) - - // Prepend the appropriate URI scheme header depending - // on the MIME type - switch mimeType { - case "image/x-icon": - base64Encoding += "data:image/x-icon;base64," - destination += ".ico" - default: - fmt.Println("Unknown MIME type: ", mimeType) + runtime.LogError(appContext, err.Error()) } + defer file.Close() - // Append the base64 encoded output - base64Encoding += base64.StdEncoding.EncodeToString(bytes) + runtime.LogDebug(a.ctx, "User opened a second instance "+strings.Join(secondInstanceArgs, ",")) + runtime.LogDebug(a.ctx, "User opened a second instance from "+secondInstanceData.WorkingDirectory) - // Write the full base64 representation of the image to file - err = os.WriteFile(destination, []byte(base64Encoding), 0644) - CheckErr(err, "Failed to write icon file", false) -} + runtime.WindowUnminimise(a.ctx) + runtime.Show(a.ctx) + go runtime.EventsEmit(a.ctx, "launchArgs", secondInstanceArgs) -func (a *App) SyncDesktop(profileName string, includeIcons bool) profile { - profile := a.GetProfile(profileName) - profile.Value = GetFileInfoSlice(getDesktopPaths()) - if includeIcons { - CopyIcons(&profile) + if len(secondInstanceArgs) != 1 { + return } - - profileJSON, err := json.Marshal(profile) - CheckErr(err, "Failed to marshal profile", false) - profileJSONStr := string(profileJSON) - - a.SaveProfile(profile.Name, profileJSONStr) - - return profile + runtime.WindowExecJS(a.ctx, "window.importIconPack('"+strings.ReplaceAll(secondInstanceArgs[0], "\\", "\\\\")+"')") } -func (a *App) GetFileInfo(profileName string) fileInfo { - defaultDirectory := getDesktopPaths()[0] - - println("Default directory: ", defaultDirectory) +func onFirstRun() { + runtime.LogInfo(appContext, "First run detected") - result, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{ - DefaultDirectory: defaultDirectory, - Title: "Select Icon", - Filters: []runtime.FileFilter{ - { - DisplayName: "Shortcuts (*.lnk)", - Pattern: "*.lnk", - }, - }, - }) - CheckErr(err, "Failed to open file dialog", false) - - println("Selected file: ", result) - - if len(result) == 0 { - return fileInfo{} - } else { - var file fs.DirEntry - - files, err := os.ReadDir(filepath.Dir(result)) - CheckErr(err, "Failed to read directory", false) - for _, f := range files { - if f.Name() == filepath.Base(result) { - file = f - break - } - } - - fileInfo, err := GetFileInfo(filepath.Dir(result), &file) - CheckErr(err, "Failed to get file info", false) - - return fileInfo - } + runtime.LogInfo(appContext, "Setting default system language") + set_system_language() } -func (a *App) GetDesktopIcons() []fileInfo { - return GetFileInfoSlice(getDesktopPaths()) +func (a *App) GetVersion() string { + return version } -func (a *App) GetProfiles() []profileInfo { - saveDir := getProfileDir() - - profiles, profilesError := os.ReadDir(saveDir) - - if profilesError != nil { - println("No profile found in ", saveDir) - } - - profileInfos := []profileInfo{} +// Send notification +func (a *App) SendNotification(title string, message string, path string, variant string) { + runtime.LogInfo(a.ctx, "Sending notification") - for _, profile := range profiles { - profileInfos = append(profileInfos, profileInfo{ - Value: profile.Name(), - Label: profile.Name(), - }) + if runtime.WindowIsNormal(a.ctx) || runtime.WindowIsMaximised(a.ctx) || runtime.WindowIsFullscreen(a.ctx) { + if path != "" { + runtime.WindowExecJS(a.ctx, `window.toast({ + title: "`+title+`", + description: "`+message+`", + path: "`+path+`", + variant: "`+variant+`" + });`) + } else { + runtime.WindowExecJS(a.ctx, `window.toast({ + title: "`+title+`", + description: "`+message+`", + variant: "`+variant+`" + });`) + } + } else { + runtime.WindowExecJS(a.ctx, `window.sendNotification("`+title+`", "`+message+`", "`+path+`", "`+variant+`")`) } - - return profileInfos } -func (a *App) GetProfile(profileName string) profile { - saveDir := getProfileDir() - - profileValue, err := os.ReadFile(filepath.Join(saveDir, profileName)) +func (a *App) SendWindowsNotification(title string, message string, path string, variant string) { + err := beeep.Notify(title, message, appIconPath) if err != nil { - fmt.Println(err) - } - - var profileInfoSlice profile - jsonErr := json.Unmarshal(profileValue, &profileInfoSlice) - if jsonErr != nil { - fmt.Println(jsonErr) - } - - valueBytes, marshalErr := json.Marshal(profileInfoSlice.Value) - if marshalErr != nil { - fmt.Println(marshalErr) + runtime.LogError(a.ctx, "Error sending notification: "+err.Error()) } - - var fileInfoSlice []fileInfo - jsonErr = json.Unmarshal(valueBytes, &fileInfoSlice) - if jsonErr != nil { - fmt.Println(jsonErr) - } - - profile := profile{ - Name: profileName, - Value: fileInfoSlice, - } - - return profile -} - -func (a *App) SaveProfile(profileName string, profile string) { - err := os.WriteFile(filepath.Join(getProfileDir(), profileName), []byte(profile), 0644) - CheckErr(err, "Failed to write profile file", false) -} - -func (a *App) GetIcon(profileName string, iconName string) string { - saveDir := filepath.Join(getBase64Dir(profileName), iconName+".ico") - - bytes, err := os.ReadFile(saveDir) - CheckErr(err, "Failed to read icon file", false) - - return string(bytes) } -func (a *App) SaveIcon(profileName string, fileInfo fileInfo) string { - var defaultDirectory string - - // check if path exists - if _, err := os.Stat(fileInfo.IconDestination); err == nil { - defaultDirectory = filepath.Dir(fileInfo.IconDestination) - } else if _, err := os.Stat(fileInfo.Destination); err == nil { - defaultDirectory = filepath.Dir(fileInfo.Destination) - } else { - defaultDirectory = getDesktopPaths()[0] - } - - println("Default directory: ", defaultDirectory) - - result, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{ - DefaultDirectory: defaultDirectory, - Title: "Select Icon", - Filters: []runtime.FileFilter{ - { - DisplayName: "*.ico", - Pattern: "*.ico", - }, - }, - }) - +func (a *App) RestartApplication(admin bool, args []string) error { + // Get the path to the current executable + executable, err := os.Executable() if err != nil { - fmt.Println(err) + runtime.LogError(a.ctx, "failed to get executable path: "+err.Error()) + return err } - println("Selected file: ", result) + if admin { + verb := "runas" + showCmd := 1 // SW_NORMAL + runtime.LogDebug(a.ctx, "Attempting to restart with elevated privileges") - if len(result) == 0 { - return "" - } else { - if fileInfo.IconName != "" { - // Delete existing icon - err := os.Remove(filepath.Join(getIconDir(profileName), fileInfo.IconName+".ico")) - if err != nil { - fmt.Println(err) - } - - // Delete base64 version - err = os.Remove(filepath.Join(getBase64Dir(profileName), fileInfo.IconName+".ico")) - if err != nil { - fmt.Println(err) - } + executablePtr, err := windows.UTF16PtrFromString(executable) + if err != nil { + runtime.LogError(a.ctx, "failed to convert executable path to UTF16: "+err.Error()) + return err } - uuid := uuid.New().String() - savePath := filepath.Join(getIconDir(profileName), uuid+".ico") + // Convert arguments to a single string + argStr := strings.Join(args, " ") - // Create dir - noFolderErr := os.MkdirAll(filepath.Dir(savePath), os.ModePerm) - if noFolderErr != nil { - fmt.Println(noFolderErr) + argPtr, err := windows.UTF16PtrFromString(argStr) + if err != nil { + runtime.LogError(a.ctx, "failed to convert arguments to UTF16: "+err.Error()) + return err } - // Copy icon - err := Copy(result, savePath) + // Execute with elevated privileges + err = windows.ShellExecute(0, windows.StringToUTF16Ptr(verb), executablePtr, argPtr, nil, int32(showCmd)) if err != nil { - fmt.Println(err) + runtime.LogError(a.ctx, "ShellExecute failed: "+err.Error()) + return fmt.Errorf("ShellExecute failed: %w", err) } - // Generate base64 version - GenerateBase64Icon(&profileName, &result, &uuid) + runtime.LogDebug(a.ctx, "Successfully requested elevated privileges") + a.beforeClose(a.ctx) - return uuid + // Exit the current process + os.Exit(0) + return nil } -} -func MatchMissingFile(fileInfo *fileInfo) { - files, err := os.ReadDir(filepath.Dir(fileInfo.Path)) - CheckErr(err, "Failed to read directory", false) - - for _, f := range files { - currentFileInfo, err := GetFileInfo(filepath.Dir((*fileInfo).Path), &f) - CheckErr(err, "Failed to get file info", false) - // Match by destination - if currentFileInfo.Destination == (*fileInfo).Destination { - fmt.Println("Found matching file: ", currentFileInfo) - // Rename currentFile to fileInfo.name - fmt.Println("Attempting to rename: ", currentFileInfo.Path, " to ", filepath.Join(filepath.Dir(currentFileInfo.Path), (*fileInfo).Name)+(*fileInfo).Extension) - err := os.Rename(currentFileInfo.Path, filepath.Join(filepath.Dir(currentFileInfo.Path), (*fileInfo).Name)+(*fileInfo).Extension) - CheckErr(err, "Failed to rename file", false) - if err == nil { - fmt.Println("Successfully renamed: ", currentFileInfo.Path, " to ", filepath.Join(filepath.Dir(currentFileInfo.Path), (*fileInfo).Name)+(*fileInfo).Extension) - } - } - } -} + // Create the new process with the same arguments as the current process + cmd := exec.Command(executable) + cmd.Args = append(cmd.Args, args...) + cmd.Env = os.Environ() + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr -func SetIcon(profileName *string, fileInfo *fileInfo) { - // Check if fileInfo.Path exists - if _, err := os.Stat(filepath.Join(filepath.Dir((*fileInfo).Path), (*fileInfo).Name) + (*fileInfo).Extension); err != nil { - fmt.Println("Failed to find file: ", (*fileInfo).Path) - fmt.Println("Attempting to match missing file: ", (*fileInfo)) - MatchMissingFile(fileInfo) - } + runtime.LogDebug(a.ctx, "Attempting to restart without elevated privileges") - // Check if type is allowed - if contains(allowedTypes, (*fileInfo).Extension) && (*fileInfo).IconName != "" { - err := os.WriteFile(path.Join(getScriptDir(), "setlnkicon.vbs"), []byte(setlnkicon), 0644) - CheckErr(err, "Failed to write setlnkicon.vbs", false) + // Start the new process + if err := cmd.Start(); err != nil { + runtime.LogError(a.ctx, "failed to start new process: "+err.Error()) + return err + } - cmd := exec.Command("cscript.exe", getScriptDir()+"\\setlnkicon.vbs", filepath.Dir((*fileInfo).Path), filepath.Base((*fileInfo).Path), getIconDir(*profileName)+"\\"+(*fileInfo).IconName+".ico", "0") + runtime.LogDebug(a.ctx, "Successfully started new process") + a.beforeClose(a.ctx) - _, err = cmd.Output() - CheckErr(err, "Failed to execute command", false) - } + // Exit the current process + os.Exit(0) + return nil } -func SetDesc(fileInfo *fileInfo) { - // Check if type is allowed - if contains(allowedTypes, (*fileInfo).Extension) && (*fileInfo).Description != "" { - err := os.WriteFile(path.Join(getScriptDir(), "setlnkdesc.vbs"), []byte(setlnkdesc), 0644) - CheckErr(err, "Failed to write setlnkdesc.vbs", false) +func checkAdminPrivileges() bool { + systemroot := os.Getenv("systemroot") - cmd := exec.Command("cscript.exe", getScriptDir()+"\\setlnkdesc.vbs", filepath.Dir((*fileInfo).Path), filepath.Base((*fileInfo).Path), (*fileInfo).Description) - - _, err = cmd.Output() - CheckErr(err, "Failed to execute command", false) + // Try to create a temporary file in the directory + tempFile, err := os.CreateTemp(systemroot, uuid.NewString()) + if err != nil { + return os.IsPermission(err) } -} + tempFile.Close() + os.Remove(tempFile.Name()) -func (a *App) RunProfile(profileName string, fileInfos []fileInfo) { - // Create scripts directory - err := os.MkdirAll(getScriptDir(), os.ModePerm) - CheckErr(err, "Failed to create script folder", false) + return false +} - for _, fileInfo := range fileInfos { - SetIcon(&profileName, &fileInfo) - SetDesc(&fileInfo) - } +func (app *App) NeedsAdminPrivileges() bool { + return NeedsAdminPrivileges } diff --git a/app_paths.go b/app_paths.go new file mode 100644 index 0000000..66dff14 --- /dev/null +++ b/app_paths.go @@ -0,0 +1,604 @@ +package main + +import ( + "embed" + "errors" + "fmt" + "io/fs" + "net/http" + "os" + "os/user" + "path/filepath" + "strconv" + "strings" + "sync" + + "github.com/google/uuid" + cmap "github.com/orcaman/concurrent-map/v2" + "github.com/wailsapp/wails/v2/pkg/runtime" +) + +//go:embed frontend/public/scripts +var scriptsFolderEmbedded embed.FS + +var setLnkIconScriptPath string +var setLnkDescScriptPath string + +var appFolder string + +var packsFolder string +var logsFolder string +var savedConfigFolder string +var activeIconFolder string +var tempFolder string +var maskFolder string +var scriptsFolder string +var externalFolder string +var configPath string +var appIconPath string + +var imageMagickFolder string +var extractIconFolder string +var imageMagickPath string +var extractIconPath string + +var tempPngPaths = cmap.New[string]() +var deletePngPaths []string = []string{} + +var selectImages = cmap.New[SelectImage]() + +type SelectImage struct { + Id string `json:"id"` + Path string `json:"path"` + TempPath string `json:"tempPath"` + HasOriginal bool `json:"hasOriginal"` + HasTemp bool `json:"hasTemp"` + IsRemoved bool `json:"isRemoved"` +} + +func path_init() error { + appData, err := os.UserConfigDir() + if err != nil { + appData = os.Getenv("APPDATA") + if appData == "" { + return errors.New("Could not find user config directory: " + err.Error()) + } + } + runtime.LogDebug(appContext, "Found user config directory: "+appData) + + appFolder = filepath.Join(appData, "iconium") + + packsFolder = filepath.Join(appFolder, "packs") + logsFolder = filepath.Join(appFolder, "logs") + savedConfigFolder = filepath.Join(appFolder, "savedconfigs") + activeIconFolder = filepath.Join(appFolder, "icons") + tempFolder = filepath.Join(appFolder, "temp") + maskFolder = filepath.Join(appFolder, "masks") + scriptsFolder = filepath.Join(appFolder, "scripts") + externalFolder = filepath.Join(appFolder, "external") + + configPath = filepath.Join(appFolder, "config.json") + appIconPath = filepath.Join(appFolder, "appicon.png") + setLnkIconScriptPath = filepath.Join(scriptsFolder, "setlnkicon.vbs") + setLnkDescScriptPath = filepath.Join(scriptsFolder, "setlnkdesc.vbs") + + runtime.LogTrace(appContext, "Attempting to create folders") + err = create_folder(appFolder) + if err != nil { + return err + } + + err = create_folder(packsFolder) + if err != nil { + return err + } + err = create_folder(logsFolder) + if err != nil { + return err + } + err = create_folder(savedConfigFolder) + if err != nil { + return err + } + err = create_folder(activeIconFolder) + if err != nil { + return err + } + err = create_folder(tempFolder) + if err != nil { + return err + } + err = create_folder(maskFolder) + if err != nil { + return err + } + err = create_folder(scriptsFolder) + if err != nil { + return err + } + err = create_folder(externalFolder) + if err != nil { + return err + } + + runtime.LogTrace(appContext, "Creating folders complete") + + runtime.LogTrace(appContext, "Attempting to create appicon") + + // Create icon from embedded appIcon if it exists + if _, err := os.Stat(appIconPath); os.IsNotExist(err) { + runtime.LogTrace(appContext, "appicon not found, creating from embedded appIcon") + err = os.WriteFile(appIconPath, appIcon, 0o644) + if err != nil { + return err + } + } + + runtime.LogTrace(appContext, "Creating appicon complete") + + imageMagickFolder = filepath.Join(externalFolder, "ImageMagick-7.1.1-35-portable-Q16-x64") + imageMagickPath = filepath.Join(imageMagickFolder, "magick.exe") + runtime.LogDebugf(appContext, "ImageMagick path: %s", imageMagickPath) + + extractIconFolder = filepath.Join(externalFolder, "ExtractIcon") + extractIconPath = filepath.Join(extractIconFolder, "extracticon.exe") + runtime.LogDebugf(appContext, "ExtractIcon path: %s", extractIconPath) + + // Copy all files in scriptsFolderEmbedded to scriptsFolder + runtime.LogTrace(appContext, "Attempting to copy scripts from embedded folder to scripts folder") + + err = fs.WalkDir(scriptsFolderEmbedded, ".", func(filePath string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + // Skip directories, but ensure they exist in the target location + if d.IsDir() { + return nil + } + + // Remove the prefix "frontend/public/scripts" from the file path + relativePath, err := filepath.Rel("frontend/public/scripts", filePath) + if err != nil { + return err + } + + // Construct the full destination path + destFile := filepath.Join(scriptsFolder, relativePath) + + // Check if destination file already exists + if _, err := os.Stat(destFile); err == nil { + return nil + } + + // Create directories if they do not exist + if err := os.MkdirAll(filepath.Dir(destFile), 0o755); err != nil { + return err + } + + // Copy the file contents to the target destination + contents, err := scriptsFolderEmbedded.ReadFile(filePath) + if err != nil { + return err + } + err = os.WriteFile(destFile, contents, 0o755) + if err != nil { + return err + } + + return nil + }) + + if err != nil { + return err + } + + runtime.LogTrace(appContext, "Copying scripts complete") + + runtime.LogTrace(appContext, "Path initialization complete") + + return nil +} + +func get_logs_folder() (string, error) { + logsFolder = filepath.Join(os.Getenv("APPDATA"), "iconium", "logs") + + // Create folder if it doesn't exist + if _, err := os.Stat(logsFolder); os.IsNotExist(err) { + err = os.MkdirAll(logsFolder, 0o755) + if err != nil { + return "", err + } + } + return logsFolder, nil +} + +func get_config_path() string { + configPath = filepath.Join(os.Getenv("APPDATA"), "iconium", "config.json") + + return configPath +} + +// Returns the desktop paths +func get_desktop_paths() (string, string) { + userDir, err := user.Current() + + if err != nil { + return "", "" + } + + homedir := userDir.HomeDir + desktop := filepath.Join(homedir, "Desktop") + + publicDir := os.Getenv("PUBLIC") + public := filepath.Join(publicDir, "Desktop") + + return desktop, public +} + +func restore_missing_external_programs() { + // URL and filename mapping + filesToDownload := map[string]string{ + "https://raw.githubusercontent.com/beyenilmez/iconium/main/build/ImageMagick-7.1.1-35-portable-Q16-x64/magick.exe": imageMagickFolder + "/magick.exe", + "https://raw.githubusercontent.com/beyenilmez/iconium/main/build/ImageMagick-7.1.1-35-portable-Q16-x64/colors.xml": imageMagickFolder + "/colors.xml", + "https://raw.githubusercontent.com/beyenilmez/iconium/main/build/ImageMagick-7.1.1-35-portable-Q16-x64/LICENSE.txt": imageMagickFolder + "/LICENSE.txt", + "https://raw.githubusercontent.com/beyenilmez/iconium/main/build/ExtractIcon/extracticon.exe": extractIconFolder + "/extracticon.exe", + "https://raw.githubusercontent.com/beyenilmez/iconium/main/build/ExtractIcon/LICENSE": extractIconFolder + "/LICENSE", + } + + // Filter out files that already exist + for url := range filesToDownload { + if exists(filesToDownload[url]) { + delete(filesToDownload, url) + } + } + + var wg sync.WaitGroup + progress := make(chan int64) // Channel for progress percentage + errors := make(chan error, len(filesToDownload)) + done := make(chan struct{}) + + // Calculate total size of files to download + totalSize := int64(0) + for url := range filesToDownload { + resp, err := http.Head(url) + if err != nil { + runtime.LogError(appContext, err.Error()) + return + } + contentLength := resp.Header.Get("Content-Length") + size, _ := strconv.ParseInt(contentLength, 10, 64) + totalSize += size + } + + runtime.LogInfo(appContext, "Total size of files to download: "+strconv.FormatInt(totalSize, 10)) + + if totalSize == 0 { + runtime.LogInfo(appContext, "No files to download") + return + } + + app.SendNotification("downloading_missing_external_programs", "", "", "info") + + // Create folders if they don't exist + if err := create_folder(imageMagickFolder); err != nil { + runtime.LogError(appContext, err.Error()) + return + } + if err := create_folder(extractIconFolder); err != nil { + runtime.LogError(appContext, err.Error()) + return + } + + runtime.LogInfo(appContext, "Downloading missing external programs...") + + // Start downloading files + for url, dest := range filesToDownload { + wg.Add(1) + go downloadFile(url, dest, progress, errors, &wg) + } + + sucessfull := true + + // Start a goroutine to track progress + go func() { + var downloadedSize int64 + for { + select { + case size := <-progress: + downloadedSize += size + percentage := float64(downloadedSize) / float64(totalSize) * 100 + runtime.LogDebugf(appContext, "Progress: %.2f%%\n", percentage) + runtime.WindowExecJS(appContext, fmt.Sprintf(`window.setProgress(%f)`, percentage)) + if downloadedSize == totalSize { + close(progress) + done <- struct{}{} + return + } + case err := <-errors: + if err != nil { + runtime.LogError(appContext, err.Error()) + sucessfull = false + } + } + } + }() + + // Wait for all downloads to complete + wg.Wait() + <-done + + if sucessfull { + app.SendNotification("downloaded_missing_external_programs", "", "", "success") + } + + runtime.WindowExecJS(appContext, `window.setProgress(0)`) + + runtime.LogInfo(appContext, "Download complete") +} + +func (a *App) ClearTempPngPaths() { + for i := range tempPngPaths.IterBuffered() { + err := os.Remove(i.Val) + if err != nil { + runtime.LogErrorf(appContext, "Error removing tempPngPath: %s", err) + continue + } + } + + tempPngPaths.Clear() + + runtime.LogDebug(appContext, "Cleared tempPngPaths") +} + +func (a *App) GetTempPngPath(id string) string { + tempPath, ok := tempPngPaths.Get(id) + if ok { + tempPath = strings.TrimPrefix(tempPath, appFolder) + return tempPath + } else { + return "" + } +} + +func (a *App) AddTempPngPath(id string, path string) { + if !contains(allowedImageExtensionsPng, filepath.Ext(path)) { + runtime.LogError(appContext, "Extension is not allowed: "+path+" ,Full path: "+path) + return + } + + oldPath, ok := tempPngPaths.Get(id) + + tempPngPath := filepath.Join(tempFolder, "iconium-"+uuid.NewString()+".png") + err := ConvertToPng(path, tempPngPath) + + if err != nil { + runtime.LogErrorf(appContext, "Error converting to png: %s", err) + return + } + + if ok { + err = os.Remove(oldPath) + if err != nil { + runtime.LogErrorf(appContext, "Error removing old temp png: %s", err) + return + } + } + + tempPngPaths.Set(id, tempPngPath) +} + +func (a *App) RemoveTempPng(id string) { + val, ok := tempPngPaths.Get(id) + if !ok { + runtime.LogWarning(appContext, "Temp png not found: "+id) + return + } + + err := os.Remove(val) + if err != nil { + runtime.LogErrorf(appContext, "Error removing temp png: %s", err) + return + } + + tempPngPaths.Remove(id) +} + +func (a *App) AddDeletePngRelativePath(relPath string) { + path := filepath.Join(appFolder, relPath) + + deletePngPaths = append(deletePngPaths, path) +} + +func (a *App) ClearDeletePngPaths() { + deletePngPaths = []string{} +} + +func (a *App) RemoveDeletePng(path string) { + paths := deletePngPaths + + for i := 0; i < len(paths); i++ { + if paths[i] == path { + paths = append(paths[:i], paths[i+1:]...) + break + } + } + + deletePngPaths = paths +} + +func (a *App) DeleteDeletePngPaths() { + for _, path := range deletePngPaths { + err := os.Remove(path) + if err != nil { + runtime.LogErrorf(appContext, "Error removing delete png: %s", err) + } + } + + deletePngPaths = []string{} +} + +func (a *App) GetSelectImage(id string, path string) SelectImage { + selectImage, ok := selectImages.Get(id) + if ok { + return selectImage + } + + fullPath := filepath.Join(appFolder, path) + + isEmpty := path == "" + + if filepath.Ext(path) != ".png" { + isEmpty = true + } else if !isEmpty { + isEmpty = !exists(fullPath) + } + + selectImage = SelectImage{ + Id: id, + Path: path, + TempPath: "", + HasOriginal: !isEmpty, + HasTemp: false, + IsRemoved: false, + } + + selectImages.Set(id, selectImage) + + return selectImage +} + +func (a *App) UploadSelectImage(id string) SelectImage { + tempPngPath := a.GetTempPng(id) + if tempPngPath == "" { + return a.GetSelectImage(id, "") + } + + selectImage := a.GetSelectImage(id, "") + + selectImage.TempPath = tempPngPath + selectImage.HasTemp = true + selectImages.Set(id, selectImage) + + return selectImage +} + +func (a *App) SetTempImage(id string, path string) error { + selectImage, ok := selectImages.Get(id) + if !ok { + return errors.New("select image not found") + } + + if selectImage.HasTemp { + a.RemoveTempPng(id) + } + + selectImage.TempPath, _ = filepath.Rel(appFolder, path) + selectImage.HasTemp = true + selectImages.Set(id, selectImage) + + runtime.LogDebugf(appContext, "Set temp image: %s", path) + + return nil +} + +func (a *App) SetSelectImage(id string, path string) { + selectImage := a.GetSelectImage(id, path) + + if !selectImage.HasTemp { + a.RemoveTempPng(id) + } + + a.AddTempPngPath(id, path) + + tempPngPath := a.GetTempPng(id) + + if tempPngPath != "" { + selectImage.TempPath, _ = filepath.Rel(appFolder, tempPngPath) + selectImage.HasTemp = true + } else { + selectImage.TempPath = "" + selectImage.HasTemp = false + } + + selectImages.Set(id, selectImage) + + runtime.LogDebugf(appContext, "Set select image: %s", path) +} + +func (a *App) ActionSelectImage(id string) SelectImage { + selectImage := a.GetSelectImage(id, "") + + if selectImage.HasOriginal { + if selectImage.HasTemp { + runtime.LogDebugf(appContext, "Removing temp png: %s", selectImage.TempPath) + + a.RemoveTempPng(id) + + selectImage.HasTemp = false + selectImage.TempPath = "" + } else { + if selectImage.IsRemoved { + runtime.LogDebugf(appContext, "Retrieving original png: %s", selectImage.Path) + + a.RemoveDeletePng(filepath.Join(appFolder, selectImage.Path)) + + selectImage.IsRemoved = false + } else { + runtime.LogDebugf(appContext, "Removing original png: %s", selectImage.Path) + + a.AddDeletePngRelativePath(selectImage.Path) + + selectImage.IsRemoved = true + } + } + } else if selectImage.HasTemp { + runtime.LogDebugf(appContext, "Removing temp png2: %s", selectImage.TempPath) + + a.RemoveTempPng(id) + + selectImage.HasTemp = false + selectImage.TempPath = "" + } + + selectImages.Set(id, selectImage) + + return selectImage +} + +func (a *App) ClearSelectImages() { + selectImages.Clear() +} + +func (a *App) SetImageIfAbsent(id string, path string) { + path = ConvertToFullPath(path) + + runtime.LogInfo(appContext, "SetImageIfAbsent path: "+path) + + if path == "" { + runtime.LogInfo(appContext, "SetImageIfAbsent: path is empty") + return + } + + selectImage := a.GetSelectImage(id, path) + if selectImage.Id == "" { + runtime.LogInfo(appContext, "SetImageIfAbsent: select image is empty") + return + } + + if !(selectImage.HasOriginal || selectImage.HasTemp) { + a.AddTempPngPath(id, path) + tempPngPath, ok := tempPngPaths.Get(id) + + if ok { + err := a.SetTempImage(id, tempPngPath) + if err != nil { + runtime.LogError(appContext, "SetImageIfAbsent: error setting temp image: "+err.Error()) + } + } else { + runtime.LogInfo(appContext, "SetImageIfAbsent: tempPngPath is empty") + } + } else { + runtime.LogInfo(appContext, "SetImageIfAbsent: hasOriginal or hasTemp is true") + } +} diff --git a/assets/screenshot-1.png b/assets/screenshot-1.png index f5c391d..7a566f9 100644 Binary files a/assets/screenshot-1.png and b/assets/screenshot-1.png differ diff --git a/assets/screenshot-2.png b/assets/screenshot-2.png new file mode 100644 index 0000000..271e31f Binary files /dev/null and b/assets/screenshot-2.png differ diff --git a/assets/screenshot-3.png b/assets/screenshot-3.png new file mode 100644 index 0000000..41bc45f Binary files /dev/null and b/assets/screenshot-3.png differ diff --git a/build/ExtractIcon/LICENSE b/build/ExtractIcon/LICENSE new file mode 100644 index 0000000..bf812af --- /dev/null +++ b/build/ExtractIcon/LICENSE @@ -0,0 +1,25 @@ +### License ### + +ExtractIcon (https://github.com/bertjohnson/extracticon) + +Licensed according to the MIT License (http://mit-license.org/). + +Copyright © Bert Johnson (https://bertjohnson.com/) of Allcloud Inc. (https://allcloud.com/). + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/build/ExtractIcon/extracticon.exe b/build/ExtractIcon/extracticon.exe new file mode 100644 index 0000000..14dab83 Binary files /dev/null and b/build/ExtractIcon/extracticon.exe differ diff --git a/build/ImageMagick-7.1.1-35-portable-Q16-x64/LICENSE.txt b/build/ImageMagick-7.1.1-35-portable-Q16-x64/LICENSE.txt new file mode 100644 index 0000000..73c0a91 --- /dev/null +++ b/build/ImageMagick-7.1.1-35-portable-Q16-x64/LICENSE.txt @@ -0,0 +1,106 @@ + ImageMagick License + https://imagemagick.org/script/license.php + +Before we get to the text of the license, lets just review what the license says in simple terms: + +It allows you to: + + * freely download and use ImageMagick software, in whole or in part, for personal, company internal, or commercial purposes; + * use ImageMagick software in packages or distributions that you create; + * link against a library under a different license; + * link code under a different license against a library under this license; + * merge code into a work under a different license; + * extend patent grants to any code using code under this license; + * and extend patent protection. + +It forbids you to: + + * redistribute any piece of ImageMagick-originated software without proper attribution; + * use any marks owned by ImageMagick Studio LLC in any way that might state or imply that ImageMagick Studio LLC endorses your distribution; + * use any marks owned by ImageMagick Studio LLC in any way that might state or imply that you created the ImageMagick software in question. + +It requires you to: + + * include a copy of the license in any redistribution you may make that includes ImageMagick software; + * provide clear attribution to ImageMagick Studio LLC for any distributions that include ImageMagick software. + +It does not require you to: + + * include the source of the ImageMagick software itself, or of any modifications you may have made to it, in any redistribution you may assemble that includes it; + * submit changes that you make to the software back to the ImageMagick Studio LLC (though such feedback is encouraged). + +A few other clarifications include: + + * ImageMagick is freely available without charge; + * you may include ImageMagick on a DVD as long as you comply with the terms of the license; + * you can give modified code away for free or sell it under the terms of the ImageMagick license or distribute the result under a different license, but you need to acknowledge the use of the ImageMagick software; + * the license is compatible with the GPL V3. + * when exporting the ImageMagick software, review its export classification. + +Terms and Conditions for Use, Reproduction, and Distribution + +The legally binding and authoritative terms and conditions for use, reproduction, and distribution of ImageMagick follow: + +Copyright @ 1999 ImageMagick Studio LLC, a non-profit organization dedicated to making software imaging solutions freely available. + +1. Definitions. + +License shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +Licensor shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +Legal Entity shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, control means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +You (or Your) shall mean an individual or Legal Entity exercising permissions granted by this License. + +Source form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +Object form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +Work shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +Derivative Works shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +Contribution shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as Not a Contribution. + +Contributor shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + * You must give any other recipients of the Work or Derivative Works a copy of this License; and + * You must cause any modified files to carry prominent notices stating that You changed the files; and + * You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + * If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. +You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an AS IS BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +How to Apply the License to your Work + +To apply the ImageMagick License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information (don't include the brackets). The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the ImageMagick License (the "License"); you may not use + this file except in compliance with the License. You may obtain a copy + of the License at + + https://imagemagick.org/script/license.php + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + License for the specific language governing permissions and limitations + under the License. diff --git a/build/ImageMagick-7.1.1-35-portable-Q16-x64/colors.xml b/build/ImageMagick-7.1.1-35-portable-Q16-x64/colors.xml new file mode 100644 index 0000000..201b735 --- /dev/null +++ b/build/ImageMagick-7.1.1-35-portable-Q16-x64/colors.xml @@ -0,0 +1,28 @@ + + + + + + +]> + + + + + + + + + + + + diff --git a/build/ImageMagick-7.1.1-35-portable-Q16-x64/magick.exe b/build/ImageMagick-7.1.1-35-portable-Q16-x64/magick.exe new file mode 100644 index 0000000..1d1ce4a Binary files /dev/null and b/build/ImageMagick-7.1.1-35-portable-Q16-x64/magick.exe differ diff --git a/build/appicon.png b/build/appicon.png index 2a2aa26..84bcdc2 100644 Binary files a/build/appicon.png and b/build/appicon.png differ diff --git a/build/default-pack/73571c68-a850-4f52-b923-3615434075a1.png b/build/default-pack/73571c68-a850-4f52-b923-3615434075a1.png new file mode 100644 index 0000000..fa58213 Binary files /dev/null and b/build/default-pack/73571c68-a850-4f52-b923-3615434075a1.png differ diff --git a/build/default-pack/files.json b/build/default-pack/files.json new file mode 100644 index 0000000..f3497fb --- /dev/null +++ b/build/default-pack/files.json @@ -0,0 +1 @@ +[{"id":"72651379-284e-4427-aad2-2fad80a3ba77","name":"Eclipse","description":"IDE for Java Developers","path":"${DESKTOP}\\**\\Eclipse*.lnk","destinationPath":"${USERPROFILE}\\eclipse\\java*\\eclipse\\eclipse.exe","extension":".lnk","hasIcon":true,"iconId":"c6c9f904-1d2e-44e4-9cef-45b1203f24bf"},{"id":"a183547f-bb5e-4743-9cd6-e2182fe1bfdb","name":"Microsoft Edge","description":"","path":"${DESKTOP}\\**\\Microsoft Edge.lnk","destinationPath":"${PROGRAMFILES(X86)}\\Microsoft\\Edge\\Application\\msedge.exe","extension":".lnk","hasIcon":true,"iconId":"6b6006e1-9435-4f88-826d-5b430d7c4b58"},{"id":"1b7d67c5-f51c-4225-bc78-c515aef0c723","name":"GitHub Desktop","description":"","path":"${DESKTOP}\\**\\GitHub Desktop.lnk","destinationPath":"${LOCALAPPDATA}\\GitHubDesktop\\GitHubDesktop.exe","extension":".lnk","hasIcon":true,"iconId":"b0e8a5f2-0312-479a-81dd-8bc12519cfe5"},{"id":"68399993-7482-43b7-8164-0e5f6f396dc8","name":"Steam","description":"Steam Client","path":"${DESKTOP}\\**\\Steam.lnk","destinationPath":"${PROGRAMFILES(X86)}\\Steam\\steam.exe","extension":".lnk","hasIcon":true,"iconId":"c55a9223-6c7d-494b-98eb-98e565b4471f"},{"id":"9c54b60a-588c-4cfc-9d3d-eef552872cc5","name":"IntelliJ IDEA","description":"IDE for Java Developers","path":"${DESKTOP}\\**\\IntelliJ IDEA.lnk","destinationPath":"${PROGRAMFILES}\\JetBrains\\IntelliJ IDEA Community Edition*\\bin\\idea64.exe","extension":".lnk","hasIcon":true,"iconId":"b1cf6f13-3391-4b4d-a459-16841e42ccb7"},{"id":"28393bcf-743c-47d7-b96e-c24d32e5d45d","name":"Discord","description":"","path":"${DESKTOP}\\**\\Discord.lnk","destinationPath":"${LOCALAPPDATA}\\Discord\\Update.exe","extension":".lnk","hasIcon":true,"iconId":"b607cca3-fd0a-4ea5-b0c0-28f2cd73347c"},{"id":"e3f31522-b0e3-4aa1-977c-ef77f832eec2","name":"Visual Studio Code","description":"","path":"${DESKTOP}\\**\\Visual Studio Code.lnk","destinationPath":"${LOCALAPPDATA}\\Programs\\Microsoft VS Code\\Code.exe","extension":".lnk","hasIcon":true,"iconId":"c6a7e324-3a3f-4f71-a6f5-2a0bca82f1e4"},{"id":"778325ab-785f-4dd9-9ff7-ed9c83436743","name":"Internet Download Manager","description":"","path":"${DESKTOP}\\**\\Internet Download Manager.lnk","destinationPath":"${PROGRAMFILES(X86)}\\Internet Download Manager\\IDMan.exe","extension":".lnk","hasIcon":true,"iconId":"0630dde6-201f-4fb3-90b9-35173dd317ee"},{"id":"d2fec138-5c89-499b-b1c8-2d77ab5f8d1c","name":"GeForce Experience","description":"","path":"${DESKTOP}\\**\\GeForce Experience.lnk","destinationPath":"${PROGRAMFILES}\\NVIDIA Corporation\\NVIDIA GeForce Experience\\NVIDIA GeForce Experience.exe","extension":".lnk","hasIcon":true,"iconId":"46d144db-254b-4f0f-ae3d-c9b524438767"},{"id":"37347aa4-6892-45b3-b43a-a04465e14c8f","name":"Logi Options+","description":"","path":"${DESKTOP}\\**\\Logi Options+.lnk","destinationPath":"${PROGRAMFILES}\\LogiOptionsPlus\\logioptionsplus.exe","extension":".lnk","hasIcon":true,"iconId":"36c2413a-34c1-4b02-8e20-1b8072e4ea38"},{"id":"dfba32d5-ab27-4713-ba4a-f4227d55f615","name":"PeaZip","description":"","path":"${DESKTOP}\\**\\PeaZip.lnk","destinationPath":"${PROGRAMFILES}\\PeaZip\\peazip.exe","extension":".lnk","hasIcon":true,"iconId":"bd639d37-743f-4fd5-adde-a108e2743dd8"},{"id":"bd6e07fc-f042-4297-bbac-fa8b6bc5d2c5","name":"AMD Software꞉ Adrenalin Edition","description":"","path":"${DESKTOP}\\**\\AMD Software꞉ Adrenalin Edition.lnk","destinationPath":"${PROGRAMFILES}\\AMD\\CNext\\CNext\\RadeonSoftware.exe","extension":".lnk","hasIcon":true,"iconId":"fc612aef-d94a-485b-9139-d0892edaafe5"},{"id":"5ccf00ca-a5fd-4700-8534-099797f9d27d","name":"Google Chrome","description":"","path":"${DESKTOP}\\**\\Google Chrome.lnk","destinationPath":"${PROGRAMFILES}\\Google\\Chrome\\Application\\chrome.exe","extension":".lnk","hasIcon":true,"iconId":"74234aff-2ab1-4f3a-9238-c108a7f9d5b2"},{"id":"94ec31b7-3d34-460a-85c9-80003294532a","name":"Ubisoft Connect","description":"Ubisoft Client","path":"${DESKTOP}\\**\\Ubisoft Connect.lnk","destinationPath":"${PROGRAMFILES(X86)}\\Ubisoft\\Ubisoft Game Launcher\\UbisoftConnect.exe","extension":".lnk","hasIcon":true,"iconId":"121ed1ae-9f82-477a-ad97-9c7f42cb1cb1"},{"id":"25e57979-c3be-45ce-a636-838b3d9aa2bf","name":"PowerToys","description":"","path":"${DESKTOP}\\**\\PowerToys*.lnk","destinationPath":"${PROGRAMFILES}\\PowerToys\\PowerToys.exe","extension":".lnk","hasIcon":true,"iconId":"c47976ba-a0f5-4922-bcee-9baebb508eb1"},{"id":"573f8804-b253-40c8-80d3-d703a2d4a2eb","name":"WO Mic Client","description":"","path":"${DESKTOP}\\**\\WO Mic*.lnk","destinationPath":"${PROGRAMFILES(X86)}\\WOMic\\WOMicClient.exe","extension":".lnk","hasIcon":true,"iconId":"2c4afaee-8a43-4e1b-8e7b-68a088a0c4c2"},{"id":"e19dd8a5-5602-4fc8-b7fa-e6f7e00881e8","name":"Youtube Music","description":"","path":"${DESKTOP}\\**\\Youtube Music.lnk","destinationPath":"${PROGRAMFILES(X86)}\\Microsoft\\Edge\\Application\\msedge_proxy.exe","extension":".lnk","hasIcon":true,"iconId":"d8cd9db5-4434-49d0-bcf3-57f487873eef"},{"id":"332adf36-394f-40a3-bbf4-6a4a614b062d","name":"Xbox","description":"","path":"${DESKTOP}\\**\\Xbox.lnk","destinationPath":"","extension":".lnk","hasIcon":true,"iconId":"8ea50d4a-bece-4e4d-95ab-b156fcedee0b"},{"id":"5cf7ea2c-3a5b-4e8f-adb6-aa77a6884bef","name":"WhatsApp","description":"","path":"${DESKTOP}\\**\\WhatsApp.lnk","destinationPath":"","extension":".lnk","hasIcon":true,"iconId":"7baf0b6d-4882-42e8-9785-1364f0ee02fc"},{"id":"5f5a0d35-ba17-4718-86e1-c12f04f833ec","name":"Bitvise SSH Client","description":"","path":"${DESKTOP}\\**\\Bitvise SSH Client.lnk","destinationPath":"${PROGRAMFILES(X86)}\\Bitvise SSH Client\\BvSsh.exe","extension":".lnk","hasIcon":true,"iconId":"83112629-8b97-48f5-8232-2a58630b1c86"},{"id":"d86fae40-7671-4f1d-bbbe-ea09c47996ca","name":"Proton VPN","description":"","path":"${DESKTOP}\\**\\Proton VPN.lnk","destinationPath":"${PROGRAMFILES}\\Proton\\VPN\\ProtonVPN.Launcher.exe","extension":".lnk","hasIcon":true,"iconId":"35f88eff-6ee5-4c1d-a60e-21e6cfbf5a65"},{"id":"43086f4d-6618-4be9-a36c-942997605d4e","name":"Epic Games Launcher","description":"","path":"${DESKTOP}\\**\\Epic Games*.lnk","destinationPath":"${PROGRAMFILES(X86)}\\Epic Games\\Launcher\\Portal\\Binaries\\Win32\\EpicGamesLauncher.exe","extension":".lnk","hasIcon":true,"iconId":"322b350b-6458-4d5a-83eb-8a7afbeee1b8"},{"id":"f67ce6c1-d5a1-46b5-bdb2-bcee09b4e981","name":"Telegram","description":"","path":"${DESKTOP}\\**\\Telegram*.lnk","destinationPath":"","extension":".lnk","hasIcon":true,"iconId":"8bfcbaca-5a82-463a-b9d9-b03b80f74004"}] diff --git a/build/default-pack/icons/1b7d67c5-f51c-4225-bc78-c515aef0c723.png b/build/default-pack/icons/1b7d67c5-f51c-4225-bc78-c515aef0c723.png new file mode 100644 index 0000000..f610a23 Binary files /dev/null and b/build/default-pack/icons/1b7d67c5-f51c-4225-bc78-c515aef0c723.png differ diff --git a/build/default-pack/icons/25e57979-c3be-45ce-a636-838b3d9aa2bf.png b/build/default-pack/icons/25e57979-c3be-45ce-a636-838b3d9aa2bf.png new file mode 100644 index 0000000..30bca14 Binary files /dev/null and b/build/default-pack/icons/25e57979-c3be-45ce-a636-838b3d9aa2bf.png differ diff --git a/build/default-pack/icons/28393bcf-743c-47d7-b96e-c24d32e5d45d.png b/build/default-pack/icons/28393bcf-743c-47d7-b96e-c24d32e5d45d.png new file mode 100644 index 0000000..d128573 Binary files /dev/null and b/build/default-pack/icons/28393bcf-743c-47d7-b96e-c24d32e5d45d.png differ diff --git a/build/default-pack/icons/332adf36-394f-40a3-bbf4-6a4a614b062d.png b/build/default-pack/icons/332adf36-394f-40a3-bbf4-6a4a614b062d.png new file mode 100644 index 0000000..b397682 Binary files /dev/null and b/build/default-pack/icons/332adf36-394f-40a3-bbf4-6a4a614b062d.png differ diff --git a/build/default-pack/icons/37347aa4-6892-45b3-b43a-a04465e14c8f.png b/build/default-pack/icons/37347aa4-6892-45b3-b43a-a04465e14c8f.png new file mode 100644 index 0000000..33fcff3 Binary files /dev/null and b/build/default-pack/icons/37347aa4-6892-45b3-b43a-a04465e14c8f.png differ diff --git a/build/default-pack/icons/43086f4d-6618-4be9-a36c-942997605d4e.png b/build/default-pack/icons/43086f4d-6618-4be9-a36c-942997605d4e.png new file mode 100644 index 0000000..8a419cb Binary files /dev/null and b/build/default-pack/icons/43086f4d-6618-4be9-a36c-942997605d4e.png differ diff --git a/build/default-pack/icons/573f8804-b253-40c8-80d3-d703a2d4a2eb.png b/build/default-pack/icons/573f8804-b253-40c8-80d3-d703a2d4a2eb.png new file mode 100644 index 0000000..622f1aa Binary files /dev/null and b/build/default-pack/icons/573f8804-b253-40c8-80d3-d703a2d4a2eb.png differ diff --git a/build/default-pack/icons/5ccf00ca-a5fd-4700-8534-099797f9d27d.png b/build/default-pack/icons/5ccf00ca-a5fd-4700-8534-099797f9d27d.png new file mode 100644 index 0000000..5acad85 Binary files /dev/null and b/build/default-pack/icons/5ccf00ca-a5fd-4700-8534-099797f9d27d.png differ diff --git a/build/default-pack/icons/5cf7ea2c-3a5b-4e8f-adb6-aa77a6884bef.png b/build/default-pack/icons/5cf7ea2c-3a5b-4e8f-adb6-aa77a6884bef.png new file mode 100644 index 0000000..7fa2c12 Binary files /dev/null and b/build/default-pack/icons/5cf7ea2c-3a5b-4e8f-adb6-aa77a6884bef.png differ diff --git a/build/default-pack/icons/5f5a0d35-ba17-4718-86e1-c12f04f833ec.png b/build/default-pack/icons/5f5a0d35-ba17-4718-86e1-c12f04f833ec.png new file mode 100644 index 0000000..df5ca17 Binary files /dev/null and b/build/default-pack/icons/5f5a0d35-ba17-4718-86e1-c12f04f833ec.png differ diff --git a/build/default-pack/icons/68399993-7482-43b7-8164-0e5f6f396dc8.png b/build/default-pack/icons/68399993-7482-43b7-8164-0e5f6f396dc8.png new file mode 100644 index 0000000..622813a Binary files /dev/null and b/build/default-pack/icons/68399993-7482-43b7-8164-0e5f6f396dc8.png differ diff --git a/build/default-pack/icons/72651379-284e-4427-aad2-2fad80a3ba77.png b/build/default-pack/icons/72651379-284e-4427-aad2-2fad80a3ba77.png new file mode 100644 index 0000000..b3e7197 Binary files /dev/null and b/build/default-pack/icons/72651379-284e-4427-aad2-2fad80a3ba77.png differ diff --git a/build/default-pack/icons/778325ab-785f-4dd9-9ff7-ed9c83436743.png b/build/default-pack/icons/778325ab-785f-4dd9-9ff7-ed9c83436743.png new file mode 100644 index 0000000..bb0edc1 Binary files /dev/null and b/build/default-pack/icons/778325ab-785f-4dd9-9ff7-ed9c83436743.png differ diff --git a/build/default-pack/icons/94ec31b7-3d34-460a-85c9-80003294532a.png b/build/default-pack/icons/94ec31b7-3d34-460a-85c9-80003294532a.png new file mode 100644 index 0000000..eaaff9c Binary files /dev/null and b/build/default-pack/icons/94ec31b7-3d34-460a-85c9-80003294532a.png differ diff --git a/build/default-pack/icons/9c54b60a-588c-4cfc-9d3d-eef552872cc5.png b/build/default-pack/icons/9c54b60a-588c-4cfc-9d3d-eef552872cc5.png new file mode 100644 index 0000000..bccf947 Binary files /dev/null and b/build/default-pack/icons/9c54b60a-588c-4cfc-9d3d-eef552872cc5.png differ diff --git a/build/default-pack/icons/a183547f-bb5e-4743-9cd6-e2182fe1bfdb.png b/build/default-pack/icons/a183547f-bb5e-4743-9cd6-e2182fe1bfdb.png new file mode 100644 index 0000000..0086a50 Binary files /dev/null and b/build/default-pack/icons/a183547f-bb5e-4743-9cd6-e2182fe1bfdb.png differ diff --git a/build/default-pack/icons/bd6e07fc-f042-4297-bbac-fa8b6bc5d2c5.png b/build/default-pack/icons/bd6e07fc-f042-4297-bbac-fa8b6bc5d2c5.png new file mode 100644 index 0000000..e69a3c4 Binary files /dev/null and b/build/default-pack/icons/bd6e07fc-f042-4297-bbac-fa8b6bc5d2c5.png differ diff --git a/build/default-pack/icons/d2fec138-5c89-499b-b1c8-2d77ab5f8d1c.png b/build/default-pack/icons/d2fec138-5c89-499b-b1c8-2d77ab5f8d1c.png new file mode 100644 index 0000000..5bd3b27 Binary files /dev/null and b/build/default-pack/icons/d2fec138-5c89-499b-b1c8-2d77ab5f8d1c.png differ diff --git a/build/default-pack/icons/d86fae40-7671-4f1d-bbbe-ea09c47996ca.png b/build/default-pack/icons/d86fae40-7671-4f1d-bbbe-ea09c47996ca.png new file mode 100644 index 0000000..46f0575 Binary files /dev/null and b/build/default-pack/icons/d86fae40-7671-4f1d-bbbe-ea09c47996ca.png differ diff --git a/build/default-pack/icons/dfba32d5-ab27-4713-ba4a-f4227d55f615.png b/build/default-pack/icons/dfba32d5-ab27-4713-ba4a-f4227d55f615.png new file mode 100644 index 0000000..f58b9dc Binary files /dev/null and b/build/default-pack/icons/dfba32d5-ab27-4713-ba4a-f4227d55f615.png differ diff --git a/build/default-pack/icons/e19dd8a5-5602-4fc8-b7fa-e6f7e00881e8.png b/build/default-pack/icons/e19dd8a5-5602-4fc8-b7fa-e6f7e00881e8.png new file mode 100644 index 0000000..60f3193 Binary files /dev/null and b/build/default-pack/icons/e19dd8a5-5602-4fc8-b7fa-e6f7e00881e8.png differ diff --git a/build/default-pack/icons/e3f31522-b0e3-4aa1-977c-ef77f832eec2.png b/build/default-pack/icons/e3f31522-b0e3-4aa1-977c-ef77f832eec2.png new file mode 100644 index 0000000..709d552 Binary files /dev/null and b/build/default-pack/icons/e3f31522-b0e3-4aa1-977c-ef77f832eec2.png differ diff --git a/build/default-pack/icons/f67ce6c1-d5a1-46b5-bdb2-bcee09b4e981.png b/build/default-pack/icons/f67ce6c1-d5a1-46b5-bdb2-bcee09b4e981.png new file mode 100644 index 0000000..77b8411 Binary files /dev/null and b/build/default-pack/icons/f67ce6c1-d5a1-46b5-bdb2-bcee09b4e981.png differ diff --git a/build/default-pack/metadata.json b/build/default-pack/metadata.json new file mode 100644 index 0000000..62335aa --- /dev/null +++ b/build/default-pack/metadata.json @@ -0,0 +1 @@ +{"id":"default-pack","name":"Default Pack","version":"v1.0.0","author":"beyenilmez","license":"MIT","description":"This is a pack providing some simple icons.","iconName":"73571c68-a850-4f52-b923-3615434075a1"} diff --git a/build/default-pack/settings.json b/build/default-pack/settings.json new file mode 100644 index 0000000..30867e3 --- /dev/null +++ b/build/default-pack/settings.json @@ -0,0 +1 @@ +{"enabled":false,"cornerRadius":20,"opacity":100} diff --git a/build/packicon.ico b/build/packicon.ico new file mode 100644 index 0000000..5266ec0 Binary files /dev/null and b/build/packicon.ico differ diff --git a/build/packicon.png b/build/packicon.png new file mode 100644 index 0000000..822535b Binary files /dev/null and b/build/packicon.png differ diff --git a/build/windows/icon.ico b/build/windows/icon.ico index 1320609..00e452b 100644 Binary files a/build/windows/icon.ico and b/build/windows/icon.ico differ diff --git a/build/windows/installer/project.nsi b/build/windows/installer/project.nsi index 654ae2e..8621eb2 100644 --- a/build/windows/installer/project.nsi +++ b/build/windows/installer/project.nsi @@ -72,7 +72,7 @@ ManifestDPIAware true Name "${INFO_PRODUCTNAME}" OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file. -InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder). +InstallDir "$Appdata\iconium\bin" # Default installing folder ($PROGRAMFILES is Program Files folder). ShowInstDetails show # This will always show the installation details. Function .onInit @@ -81,13 +81,24 @@ FunctionEnd Section !insertmacro wails.setShellContext + SetShellVarContext current !insertmacro wails.webview2runtime SetOutPath $INSTDIR - !insertmacro wails.files + SetOutPath "$AppData\iconium\external\ImageMagick-7.1.1-35-portable-Q16-x64" + File /r "..\..\ImageMagick-7.1.1-35-portable-Q16-x64\*.*" + + SetOutPath "$AppData\iconium\external\ExtractIcon" + File /r "..\..\ExtractIcon\*.*" + + SetOutPath "$AppData\iconium\packs\default-pack" + File /r "..\..\default-pack\*" + + SetOutPath $INSTDIR + CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" @@ -99,8 +110,12 @@ SectionEnd Section "uninstall" !insertmacro wails.setShellContext + SetShellVarContext current - RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath + RMDir /r "$AppData\iconium\EBWebView" # Remove the WebView2 DataPath + RMDir /r "$AppData\iconium\scripts" # Remove scripts + RMDir /r "$AppData\iconium\external" # Remove external programs + RMDir /r "$AppData\iconium\temp" # Remove temp folder RMDir /r $INSTDIR diff --git a/build/windows/installer/wails_tools.nsh b/build/windows/installer/wails_tools.nsh deleted file mode 100644 index f9c0f88..0000000 --- a/build/windows/installer/wails_tools.nsh +++ /dev/null @@ -1,249 +0,0 @@ -# DO NOT EDIT - Generated automatically by `wails build` - -!include "x64.nsh" -!include "WinVer.nsh" -!include "FileFunc.nsh" - -!ifndef INFO_PROJECTNAME - !define INFO_PROJECTNAME "{{.Name}}" -!endif -!ifndef INFO_COMPANYNAME - !define INFO_COMPANYNAME "{{.Info.CompanyName}}" -!endif -!ifndef INFO_PRODUCTNAME - !define INFO_PRODUCTNAME "{{.Info.ProductName}}" -!endif -!ifndef INFO_PRODUCTVERSION - !define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}" -!endif -!ifndef INFO_COPYRIGHT - !define INFO_COPYRIGHT "{{.Info.Copyright}}" -!endif -!ifndef PRODUCT_EXECUTABLE - !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe" -!endif -!ifndef UNINST_KEY_NAME - !define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" -!endif -!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}" - -!ifndef REQUEST_EXECUTION_LEVEL - !define REQUEST_EXECUTION_LEVEL "admin" -!endif - -RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" - -!ifdef ARG_WAILS_AMD64_BINARY - !define SUPPORTS_AMD64 -!endif - -!ifdef ARG_WAILS_ARM64_BINARY - !define SUPPORTS_ARM64 -!endif - -!ifdef SUPPORTS_AMD64 - !ifdef SUPPORTS_ARM64 - !define ARCH "amd64_arm64" - !else - !define ARCH "amd64" - !endif -!else - !ifdef SUPPORTS_ARM64 - !define ARCH "arm64" - !else - !error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY" - !endif -!endif - -!macro wails.checkArchitecture - !ifndef WAILS_WIN10_REQUIRED - !define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later." - !endif - - !ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED - !define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}" - !endif - - ${If} ${AtLeastWin10} - !ifdef SUPPORTS_AMD64 - ${if} ${IsNativeAMD64} - Goto ok - ${EndIf} - !endif - - !ifdef SUPPORTS_ARM64 - ${if} ${IsNativeARM64} - Goto ok - ${EndIf} - !endif - - IfSilent silentArch notSilentArch - silentArch: - SetErrorLevel 65 - Abort - notSilentArch: - MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}" - Quit - ${else} - IfSilent silentWin notSilentWin - silentWin: - SetErrorLevel 64 - Abort - notSilentWin: - MessageBox MB_OK "${WAILS_WIN10_REQUIRED}" - Quit - ${EndIf} - - ok: -!macroend - -!macro wails.files - !ifdef SUPPORTS_AMD64 - ${if} ${IsNativeAMD64} - File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}" - ${EndIf} - !endif - - !ifdef SUPPORTS_ARM64 - ${if} ${IsNativeARM64} - File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}" - ${EndIf} - !endif -!macroend - -!macro wails.writeUninstaller - WriteUninstaller "$INSTDIR\uninstall.exe" - - SetRegView 64 - WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}" - WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}" - WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}" - WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}" - WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" - WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S" - - ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 - IntFmt $0 "0x%08X" $0 - WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0" -!macroend - -!macro wails.deleteUninstaller - Delete "$INSTDIR\uninstall.exe" - - SetRegView 64 - DeleteRegKey HKLM "${UNINST_KEY}" -!macroend - -!macro wails.setShellContext - ${If} ${REQUEST_EXECUTION_LEVEL} == "admin" - SetShellVarContext all - ${else} - SetShellVarContext current - ${EndIf} -!macroend - -# Install webview2 by launching the bootstrapper -# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment -!macro wails.webview2runtime - !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT - !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime" - !endif - - SetRegView 64 - # If the admin key exists and is not empty then webview2 is already installed - ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" - ${If} $0 != "" - Goto ok - ${EndIf} - - ${If} ${REQUEST_EXECUTION_LEVEL} == "user" - # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed - ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" - ${If} $0 != "" - Goto ok - ${EndIf} - ${EndIf} - - SetDetailsPrint both - DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}" - SetDetailsPrint listonly - - InitPluginsDir - CreateDirectory "$pluginsdir\webview2bootstrapper" - SetOutPath "$pluginsdir\webview2bootstrapper" - File "tmp\MicrosoftEdgeWebview2Setup.exe" - ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install' - - SetDetailsPrint both - ok: -!macroend - -# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b -!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND - ; Backup the previously associated file class - ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" "" - WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0" - - WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}" - - WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}` - WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}` - WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open" - WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}` - WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}` -!macroend - -!macro APP_UNASSOCIATE EXT FILECLASS - ; Backup the previously associated file class - ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup` - WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0" - - DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}` -!macroend - -!macro wails.associateFiles - ; Create file associations - {{range .Info.FileAssociations}} - !insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\"" - - File "..\{{.IconName}}.ico" - {{end}} -!macroend - -!macro wails.unassociateFiles - ; Delete app associations - {{range .Info.FileAssociations}} - !insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}" - - Delete "$INSTDIR\{{.IconName}}.ico" - {{end}} -!macroend - -!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND - DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" - WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}" - WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" "" - WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}" - WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" "" - WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" "" - WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}" -!macroend - -!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL - DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}" -!macroend - -!macro wails.associateCustomProtocols - ; Create custom protocols associations - {{range .Info.Protocols}} - !insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\"" - - {{end}} -!macroend - -!macro wails.unassociateCustomProtocols - ; Delete app custom protocol associations - {{range .Info.Protocols}} - !insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}" - {{end}} -!macroend diff --git a/build/windows/packicon.ico b/build/windows/packicon.ico new file mode 100644 index 0000000..bb22703 Binary files /dev/null and b/build/windows/packicon.ico differ diff --git a/config.go b/config.go new file mode 100644 index 0000000..8d3988e --- /dev/null +++ b/config.go @@ -0,0 +1,303 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "reflect" + "strconv" + + "github.com/wailsapp/wails/v2/pkg/runtime" +) + +type Config struct { + Theme *string `json:"theme"` // system, light, dark + ColorScheme *string `json:"colorScheme"` // default, midnightAsh + UseSystemTitleBar *bool `json:"useSystemTitleBar"` // true, false + EnableLogging *bool `json:"enableLogging"` // true, false + EnableTrace *bool `json:"enableTrace"` // true, false + EnableDebug *bool `json:"enableDebug"` // true, false + EnableInfo *bool `json:"enableInfo"` // true, false + EnableWarn *bool `json:"enableWarn"` // true, false + EnableError *bool `json:"enableError"` // true, false + EnableFatal *bool `json:"enableFatal"` // true, false + MaxLogFiles *int `json:"maxLogFiles"` // int + Language *string `json:"language"` // en-US, tr-TR + SaveWindowStatus *bool `json:"saveWindowStatus"` // true, false + WindowStartState *int `json:"windowStartState"` // 0 = Normal, 1 = Maximized, 2 = Minimized, 3 = Fullscreen + WindowStartPositionX *int `json:"windowStartPositionX"` // x + WindowStartPositionY *int `json:"windowStartPositionY"` // y + WindowStartSizeX *int `json:"windowStartSizeX"` // x + WindowStartSizeY *int `json:"windowStartSizeY"` // y + WindowScale *int `json:"windowScale"` // % + Opacity *int `json:"opacity"` // % + WindowEffect *int `json:"windowEffect"` // 0 = Auto, 1 = None, 2 = Mica, 3 = Acrylic, 4 = Tabbed + CheckForUpdates *bool `json:"checkForUpdates"` // true, false + LastUpdateCheck *int `json:"lastUpdateCheck"` // unix timestamp + MatchLnkByDestination *bool `json:"matchLnkByDestination"` // true, false + MatchURLByDestination *bool `json:"matchURLByDestination"` // true, false + RenameMatchedFiles *bool `json:"renameMatchedFiles"` // true, false + ChangeDescriptionOfMathcedLnkFiles *bool `json:"changeDescriptionOfMathcedLnkFiles"` // true, false +} + +func GetDefaultConfig() Config { + defaultTheme := "system" + defaultColorScheme := "default" + defaultUseSystemTitleBar := false + defaultEnableLogging := true + defaultEnableTrace := false + defaultEnableDebug := false + defaultEnableInfo := true + defaultEnableWarn := true + defaultEnableError := true + defaultEnableFatal := true + defaultMaxLogFiles := 20 + defaultLanguage := "en-US" + defaultSaveWindowStatus := true + defaultWindowStartState := 0 + defaultWindowStartPositionX := -100000 + defaultWindowStartPositionY := -100000 + defaultWindowStartSizeX := -100000 + defaultWindowStartSizeY := -100000 + defaultWindowScale := 100 + defaultOpacity := 80 + defaultWindowEffect := 3 + defaultCheckForUpdates := true + defaultLastUpdateCheck := 0 + defaultMatchLnkByDestination := true + defaultMatchURLByDestination := true + defaultRenameMatchedFiles := false + defaultChangeDescriptionOfMathcedLnkFiles := false + + return Config{ + Theme: &defaultTheme, + ColorScheme: &defaultColorScheme, + UseSystemTitleBar: &defaultUseSystemTitleBar, + EnableLogging: &defaultEnableLogging, + EnableTrace: &defaultEnableTrace, + EnableDebug: &defaultEnableDebug, + EnableInfo: &defaultEnableInfo, + EnableWarn: &defaultEnableWarn, + EnableError: &defaultEnableError, + EnableFatal: &defaultEnableFatal, + MaxLogFiles: &defaultMaxLogFiles, + Language: &defaultLanguage, + SaveWindowStatus: &defaultSaveWindowStatus, + WindowStartState: &defaultWindowStartState, + WindowStartPositionX: &defaultWindowStartPositionX, + WindowStartPositionY: &defaultWindowStartPositionY, + WindowStartSizeX: &defaultWindowStartSizeX, + WindowStartSizeY: &defaultWindowStartSizeY, + WindowScale: &defaultWindowScale, + Opacity: &defaultOpacity, + WindowEffect: &defaultWindowEffect, + CheckForUpdates: &defaultCheckForUpdates, + LastUpdateCheck: &defaultLastUpdateCheck, + MatchLnkByDestination: &defaultMatchLnkByDestination, + MatchURLByDestination: &defaultMatchURLByDestination, + RenameMatchedFiles: &defaultRenameMatchedFiles, + ChangeDescriptionOfMathcedLnkFiles: &defaultChangeDescriptionOfMathcedLnkFiles, + } +} + +var config Config = GetDefaultConfig() + +func config_init() error { + err := CreateConfigIfNotExist() + if err != nil { + return errors.New("failed to create config file") + } + err = ReadConfig(configPath) + if err != nil { + config = GetDefaultConfig() + } else { + merge_defaults() + } + + return nil +} + +func merge_defaults() { + defaultConfig := GetDefaultConfig() + + fmt.Println("Merging default config") + + v := reflect.ValueOf(&config).Elem() + t := v.Type() + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + fieldName := field.Name + fieldValue := v.FieldByName(fieldName) + + if fieldValue.Kind() == reflect.Ptr && fieldValue.IsNil() { + // If config's field is nil, set it to the default value's field + defaultValue := reflect.ValueOf(&defaultConfig).Elem().FieldByName(fieldName) + fieldValue.Set(defaultValue) + } + } +} + +func (app *App) GetConfig() Config { + return config +} + +func (app *App) GetConfigField(fieldName string) interface{} { + runtime.LogDebug(app.ctx, fmt.Sprintf("Attempting to get config field %s", fieldName)) + + // Get the reflection Type and Value of the Config struct + v := reflect.ValueOf(&config).Elem() + t := v.Type() + + // Find the field by name + _, found := t.FieldByName(fieldName) + if !found { + runtime.LogWarning(app.ctx, fmt.Sprintf("Unknown config field: %s", fieldName)) + return "undefined" + } + + // Get the field value + fieldValue := v.FieldByName(fieldName) + + // Check if the field is a pointer + if fieldValue.Kind() == reflect.Ptr { + if fieldValue.IsNil() { + runtime.LogWarning(app.ctx, fmt.Sprintf("Config field %s is nil", fieldName)) + return "undefined" + } + // Dereference the pointer + fieldValue = fieldValue.Elem() + } + + runtime.LogDebug(app.ctx, fmt.Sprintf("Config field %s has value: %v", fieldName, fieldValue.Interface())) + return fieldValue.Interface() +} + +func (app *App) SetConfigField(fieldName string, value interface{}) { + runtime.LogDebug(app.ctx, fmt.Sprintf("Attempting to set config field %s to %v", fieldName, value)) + + v := reflect.ValueOf(&config).Elem() + t := v.Type() + + _, found := t.FieldByName(fieldName) + if !found { + runtime.LogWarning(app.ctx, fmt.Sprintf("Unknown config field: %s", fieldName)) + return + } + + fieldValue := v.FieldByName(fieldName) + + if !fieldValue.IsValid() { + runtime.LogWarning(app.ctx, fmt.Sprintf("Invalid field: %s", fieldName)) + return + } + + if fieldValue.Kind() == reflect.Ptr { + runtime.LogDebug(app.ctx, fmt.Sprintf("Dereferencing config field %s", fieldName)) + fieldValue = fieldValue.Elem() + } + + runtime.LogDebug(app.ctx, fmt.Sprintf("Config field %s type: %v", fieldName, fieldValue.Kind())) + + switch fieldValue.Kind() { + case reflect.String: + strVal, ok := value.(string) + if !ok { + runtime.LogWarning(app.ctx, fmt.Sprintf("Invalid value type for string field %s: %v", fieldName, value)) + return + } + fieldValue.SetString(strVal) + + case reflect.Bool: + boolVal, ok := value.(bool) + if !ok { + runtime.LogWarning(app.ctx, fmt.Sprintf("Invalid value type for boolean field %s: %v", fieldName, value)) + return + } + fieldValue.SetBool(boolVal) + + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + intVal, err := strconv.Atoi(fmt.Sprintf("%v", value)) + if err != nil { + runtime.LogWarning(app.ctx, fmt.Sprintf("Invalid value type for integer field %s: %v", fieldName, value)) + return + } + fieldValue.SetInt(int64(intVal)) + + case reflect.Float32, reflect.Float64: + floatVal, ok := value.(float64) + if !ok { + runtime.LogWarning(app.ctx, fmt.Sprintf("Invalid value type for float field %s: %v", fieldName, value)) + return + } + fieldValue.SetFloat(floatVal) + + case reflect.Slice: + sliceVal, ok := value.([]string) + if !ok { + runtime.LogWarning(app.ctx, fmt.Sprintf("Invalid value type for slice field %s: %v", fieldName, value)) + return + } + slice := reflect.ValueOf(sliceVal) + fieldValue.Set(slice) + + default: + runtime.LogWarning(app.ctx, fmt.Sprintf("Unsupported field type for field %s of type %s", fieldName, fieldValue.Kind())) + return + } + + runtime.LogDebug(app.ctx, fmt.Sprintf("Config field %s set to %v", fieldName, fieldValue.Interface())) +} + +// Creates a default config at configPath if none exists +func CreateConfigIfNotExist() error { + configPath = get_config_path() + + if _, err := os.Stat(configPath); os.IsNotExist(err) { + config = GetDefaultConfig() + } + return nil +} + +// WriteConfig writes the current config to the path +func WriteConfig(path string) error { + file, err := os.Create(path) + if err != nil { + return err + } + defer file.Close() + + encoder := json.NewEncoder(file) + err = encoder.Encode(config) + if err != nil { + return err + } + + return nil +} + +// Read config from path +func ReadConfig(path string) error { + file, err := os.Open(path) + + if err != nil { + return err + } + + defer file.Close() + decoder := json.NewDecoder(file) + + config = Config{} + + err = decoder.Decode(&config) + if err != nil { + return err + } + + return nil +} + +func (app *App) ReadConfig(path string) error { + return ReadConfig(path) +} diff --git a/dialog.go b/dialog.go new file mode 100644 index 0000000..a5cac1c --- /dev/null +++ b/dialog.go @@ -0,0 +1,362 @@ +package main + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/google/uuid" + "github.com/wailsapp/wails/v2/pkg/runtime" +) + +func (a *App) SaveConfigDialog() { + path, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{ + Title: "Save configuration", + DefaultDirectory: savedConfigFolder, + DefaultFilename: "config.json", + CanCreateDirectories: true, + Filters: []runtime.FileFilter{ + { + DisplayName: "JSON", + Pattern: "*.json", + }, + }, + }) + + if err != nil { + runtime.LogWarning(a.ctx, err.Error()) + return + } + + err = WriteConfig(path) + + if err != nil { + if path == "" { + runtime.LogInfo(a.ctx, "No path given, not saving config") + return + } + runtime.LogWarning(a.ctx, err.Error()) + a.SendNotification("", "settings.there_was_an_error_saving_the_config", "", "error") + return + } + + runtime.LogInfo(a.ctx, "Config saved to "+path) + path = strings.ReplaceAll(path, "\\", "\\\\") + a.SendNotification("", "settings.config_saved", path, "success") +} + +func (a *App) GetLoadConfigPath() string { + path, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{ + Title: "Load configuration", + DefaultDirectory: savedConfigFolder, + CanCreateDirectories: true, + Filters: []runtime.FileFilter{ + { + DisplayName: "JSON", + Pattern: "*.json", + }, + }, + }) + + if err != nil { + runtime.LogWarning(a.ctx, err.Error()) + return "" + } + + return path +} + +func (a *App) GetBase64Png() string { + path, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{ + Title: "Select image", + CanCreateDirectories: true, + Filters: []runtime.FileFilter{ + { + DisplayName: "PNG", + Pattern: "*.png", + }, + }, + }) + + if err != nil { + runtime.LogWarning(a.ctx, err.Error()) + return "" + } + + if path == "" { + return "" + } + + base64Png := GenerateBase64PngFromPath(path) + + runtime.LogInfo(a.ctx, "Image selected: "+path) + + return base64Png +} + +func (a *App) GetTempPng(id string) string { + oldPackPngPath, ok := tempPngPaths.Get(id) + + tempPackPngPath := filepath.Join(tempFolder, "iconium-"+uuid.NewString()+".png") + + path, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{ + Title: "Select image", + Filters: []runtime.FileFilter{ + { + DisplayName: "Image File", + Pattern: "*.png;*.jpg;*.jpeg;*.webp;*.svg;*.bmp;*.ico;*.exe;*.lnk;*.url", + }, + }, + }) + + if err != nil { + runtime.LogWarning(a.ctx, err.Error()) + return "" + } + if path == "" { + return "" + } + + err = ConvertToPng(path, tempPackPngPath) + if err != nil { + runtime.LogErrorf(a.ctx, "Error copying pack.png file: %s", err.Error()) + return "" + } + + runtime.LogInfof(a.ctx, "Trimming path: %s (cut %s)", tempPackPngPath, appFolder) + trimmedPath := strings.TrimPrefix(tempPackPngPath, appFolder) + + tempPngPaths.Set(id, tempPackPngPath) + + // Remove old pack png + if ok { + os.Remove(oldPackPngPath) + } + + return trimmedPath +} + +func (a *App) GetIconFolder() string { + path, err := runtime.OpenDirectoryDialog(a.ctx, runtime.OpenDialogOptions{ + Title: "Select folder", + CanCreateDirectories: true, + }) + + if err != nil { + runtime.LogWarning(a.ctx, err.Error()) + return "" + } + + return path +} + +func (a *App) GetIconFile() string { + path, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{ + Title: "Select file", + CanCreateDirectories: true, + Filters: []runtime.FileFilter{ + { + DisplayName: "Shortcut", + Pattern: "*.lnk;*.url", + }, + }, + }) + + if err != nil { + runtime.LogWarning(a.ctx, err.Error()) + return "" + } + + return path +} + +func (a *App) GetIconFiles() []string { + paths, err := runtime.OpenMultipleFilesDialog(a.ctx, runtime.OpenDialogOptions{ + Title: "Select file", + CanCreateDirectories: true, + Filters: []runtime.FileFilter{ + { + DisplayName: "Shortcut", + Pattern: "*.lnk;*.url", + }, + }, + }) + + if err != nil { + runtime.LogWarning(a.ctx, err.Error()) + return nil + } + + return paths +} + +func (a *App) GetFilePath(generalPath string) string { + fullPath := ConvertToFullPath(filepath.Dir(generalPath)) + + path, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{ + Title: "Select file", + DefaultDirectory: fullPath, + ResolvesAliases: true, + Filters: []runtime.FileFilter{ + { + DisplayName: "Shortcut", + Pattern: "*.lnk;*.url", + }, + }, + }) + + if err != nil { + runtime.LogWarning(a.ctx, err.Error()) + return "" + } + + return ConvertToGeneralPath(path) +} + +func (a *App) ExportIconPack(packId string) string { + // Check if the icon pack exists + _, err := a.GetIconPack(packId) + if err != nil { + runtime.LogErrorf(appContext, "Icon pack %s not found: %s", packId, err.Error()) + return "" + } + + path, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{ + Title: "Export icon pack", + CanCreateDirectories: true, + Filters: []runtime.FileFilter{ + { + DisplayName: "Iconium File", + Pattern: "*.icnm", + }, + }, + }) + if err != nil { + runtime.LogWarning(a.ctx, err.Error()) + return "" + } + + iconPackPath := filepath.Join(packsFolder, packId) + runtime.LogInfof(a.ctx, "Exporting icon pack: %s", iconPackPath) + + err = zip_folder(iconPackPath, path) + + if err != nil { + runtime.LogErrorf(a.ctx, "Error exporting icon pack: %s", err.Error()) + return "" + } + + a.SendNotification("settings.icon_pack.exported", "", path, "success") + + return path +} + +func (a *App) GetIconPackPath() string { + path, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{ + Title: "Import icon pack", + CanCreateDirectories: true, + Filters: []runtime.FileFilter{ + { + DisplayName: "Iconium File", + Pattern: "*.icnm", + }, + }, + }) + if err != nil { + runtime.LogWarning(a.ctx, err.Error()) + return "" + } + + return path +} + +func (a *App) ImportIconPack(path string) string { + runtime.LogInfof(a.ctx, "Importing icon pack: %s", path) + + extractFolder := filepath.Join(tempFolder, "iconium-"+uuid.NewString()) + defer os.RemoveAll(extractFolder) + + err := unzip_folder(path, extractFolder) + if err != nil { + runtime.LogErrorf(a.ctx, "Error importing icon pack: %s", err.Error()) + return "" + } + + files, err := os.ReadDir(extractFolder) + if err != nil { + runtime.LogErrorf(a.ctx, "Error importing icon pack: %s", err.Error()) + return "" + } + if len(files) != 1 { + runtime.LogErrorf(a.ctx, "Error importing icon pack") + return "" + } + + packId := uuid.NewString() + + tempIconPackPath := filepath.Join(extractFolder, files[0].Name()) + targetPath := filepath.Join(packsFolder, packId) + + err = os.Rename(tempIconPackPath, targetPath) + if err != nil { + runtime.LogErrorf(a.ctx, "Error importing icon pack: %s", err.Error()) + return "" + } + + a.SendNotification("my_packs.import_pack.success", "", "", "success") + + return packId +} + +func (a *App) GetIcnmMetadata(path string) Metadata { + extractFolder := filepath.Join(tempFolder, "iconium-"+uuid.NewString()) + + err := unzip_folder(path, extractFolder) + if err != nil { + runtime.LogErrorf(a.ctx, "Error importing icon pack: %s", err.Error()) + return Metadata{} + } + defer os.RemoveAll(extractFolder) + + files, err := os.ReadDir(extractFolder) + if err != nil { + runtime.LogErrorf(a.ctx, "Error importing icon pack: %s", err.Error()) + return Metadata{} + } + if len(files) != 1 { + runtime.LogErrorf(a.ctx, "Error importing icon pack") + return Metadata{} + } + + metadataFile := filepath.Join(extractFolder, files[0].Name(), "metadata.json") + + var metadata Metadata + err = readJSON(metadataFile, &metadata) + if err != nil { + runtime.LogErrorf(a.ctx, "Error importing icon pack: %s", err.Error()) + return Metadata{} + } + + iconPath := filepath.Join(extractFolder, files[0].Name(), metadata.IconName+".png") + tempIconName := "iconium-" + uuid.NewString() + ".png" + tempIconPath := filepath.Join(tempFolder, tempIconName) + + err = copy_file(iconPath, tempIconPath) + if err != nil { + runtime.LogWarningf(a.ctx, "Error copying icon: %s", err.Error()) + } + + metadata.IconName = "temp\\" + tempIconName + + tempPngPaths.Set(uuid.NewString(), tempIconPath) + + return metadata +} + +func (a *App) OpenFileInExplorer(path string) { + runtime.LogInfo(a.ctx, "Opening file in explorer: "+path) + + cmd := exec.Command(`explorer`, `/select,`, path) + cmd.Run() +} diff --git a/frontend/index.html b/frontend/index.html index 38fe32c..9b1e671 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,8 +2,9 @@ + - Desktop Manager + Iconium

diff --git a/frontend/package.json b/frontend/package.json index 6ddcb42..81d76fb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,5 +1,5 @@ { - "name": "frontend", + "name": "iconium", "private": true, "version": "0.0.0", "type": "module", @@ -10,39 +10,56 @@ "preview": "vite preview" }, "dependencies": { - "@hookform/resolvers": "^3.3.4", - "@radix-ui/react-accordion": "^1.1.2", - "@radix-ui/react-dialog": "^1.0.5", - "@radix-ui/react-dropdown-menu": "^2.0.6", - "@radix-ui/react-label": "^2.0.2", - "@radix-ui/react-popover": "^1.0.7", - "@radix-ui/react-slot": "^1.0.2", - "class-variance-authority": "^0.6.0", - "clsx": "^1.2.1", - "cmdk": "^0.2.1", - "lucide-react": "^0.252.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-hook-form": "^7.51.0", - "tailwind-merge": "^1.13.2", - "tailwindcss-animate": "^1.0.6", - "zod": "^3.22.4" + "@hookform/resolvers": "^3.9.0", + "@radix-ui/react-accordion": "^1.2.0", + "@radix-ui/react-checkbox": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.1", + "@radix-ui/react-hover-card": "^1.1.1", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-popover": "^1.1.1", + "@radix-ui/react-progress": "^1.1.0", + "@radix-ui/react-slider": "^1.2.0", + "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-switch": "^1.1.0", + "@radix-ui/react-tabs": "^1.1.0", + "@radix-ui/react-toast": "^1.2.1", + "@radix-ui/react-toggle": "^1.1.0", + "@radix-ui/react-toggle-group": "^1.1.0", + "@radix-ui/react-tooltip": "^1.1.2", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "cmdk": "^1.0.0", + "html-react-parser": "^5.1.15", + "i18next": "^23.11.5", + "i18next-browser-languagedetector": "^8.0.0", + "i18next-http-backend": "^2.5.2", + "lucide-react": "^0.396.0", + "next-themes": "^0.3.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-hook-form": "^7.52.1", + "react-i18next": "^14.1.2", + "sonner": "^1.5.0", + "tailwind-merge": "^2.3.0", + "tailwindcss-animate": "^1.0.7", + "zod": "^3.23.8" }, "devDependencies": { - "@types/react": "^18.0.37", - "@types/react-dom": "^18.0.11", - "@typescript-eslint/eslint-plugin": "^5.59.0", - "@typescript-eslint/parser": "^5.59.0", - "@vitejs/plugin-react": "^4.0.0", - "autoprefixer": "^10.4.12", - "eslint": "^8.38.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.3.4", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^7.13.1", + "@typescript-eslint/parser": "^7.13.1", + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.19", + "eslint": "^9.5.0", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.7", "path": "^0.12.7", - "postcss": "^8.4.17", - "tailwindcss": "^3.1.8", - "typescript": "^5.0.2", - "url": "^0.11.1", - "vite": "^4.3.9" + "postcss": "^8.4.38", + "tailwind-scrollbar": "^3.1.0", + "tailwindcss": "^3.4.4", + "typescript": "^5.5.2", + "url": "^0.11.3", + "vite": "^4.0.0" } } diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 deleted file mode 100644 index 50d2b91..0000000 --- a/frontend/package.json.md5 +++ /dev/null @@ -1 +0,0 @@ -7be0872a8fdf54877448dff68fb922a2 \ No newline at end of file diff --git a/frontend/public/locales/en-US.json b/frontend/public/locales/en-US.json new file mode 100644 index 0000000..f659866 --- /dev/null +++ b/frontend/public/locales/en-US.json @@ -0,0 +1,326 @@ +{ + "save": "Save", + "cancel": "Cancel", + "yes": "Yes", + "no": "No", + "show_in_explorer": "Show in Explorer", + "import": "Import", + "export": "Export", + "error": "Error", + "show": "Show", + "create": "Create", + "delete": "Delete", + + "downloading_missing_external_programs": "Downloading missing external programs...", + "downloaded_missing_external_programs": "Successfully downloaded missing external programs.", + + "nav": { + "my_packs": "My Packs", + "settings": "Settings" + }, + + "file_info": { + "name": { + "label": "Name", + "placeholder": "File name", + "help": "This is the new name that will be applied to shortcuts that do not already have this name." + }, + + "description": { + "label": "Description", + "placeholder": "File description", + "help": "This is the description that will be set for the shortcut." + }, + + "path": { + "label": "Path", + "placeholder": "File path", + "help": "This is the path where the icon will be applied.", + "rules": "" + }, + + "destination": { + "label": "Destination", + "placeholder": "File destination", + "help": "This is the path that will be matched with destination paths of shortcuts in the source folder if the file is not found in the original path." + }, + + "url": { + "label": "URL", + "placeholder": "URL", + "help": "This is the URL that will be matched with URLs of files in the source folder if the file is not found in the original path." + } + }, + + "my_packs": { + "create_new_pack": { + "title": "Create Icon Pack" + }, + + "delete_pack": { + "confirmation_title": "Are you sure you want to delete this pack?", + "confirmation_description": "This action cannot be undone.", + "delete_generated_icons": "Delete Generated Icons" + }, + + "import_pack": { + "confirmation_title": "Do you want to import this pack?", + "success": "Pack imported successfully." + }, + + "edit_pack": { + "path_found": "Path found" + }, + + "buttons": { + "add_pack": { + "tooltip": "Add new pack" + }, + + "reload_packs": { + "tooltip": "Reload packs" + }, + + "import_pack": { + "tooltip": "Import pack" + } + }, + + "card": { + "pack_information": { + "label": "Pack Information", + + "information": { + "name": { + "label": "Name", + "placeholder": "My Pack", + + "message": { + "name_required": "Name is required", + "name_max": "Name can not exceed 32 characters" + } + }, + + "version": { + "label": "Version", + "placeholder": "v1.0.0", + + "message": { + "version_required": "Version is required", + "version_format": "Version must be in the format v1.0.0" + } + }, + + "author": { + "label": "Author", + "placeholder": "Your name", + + "message": { + "author_max": "Author can not exceed 32 characters" + } + }, + + "license": { + "label": "License", + "placeholder": "MIT", + + "message": { + "license_max": "License can not exceed 64 characters" + } + }, + + "description": { + "label": "Description", + "placeholder": "This is my icon pack", + + "message": { + "description_required": "Description is required", + "description_max": "Description can not exceed 1024 characters" + } + }, + + "icon": { + "label": "Icon" + } + } + }, + + "pack_actions": { + "label": "Pack Actions", + "admin_warning": "Some icons are only applied when app is runned as administrator.", + "restart_as_admin": "Restart as Administrator", + "apply_icon_pack": "Apply", + "edit_icon_pack": "Edit", + "export_icon_pack": "Export", + "add_icons_from_desktop": "Add Icons from Desktop", + "add_icons": "Add Icons", + "add_folder": "Add Folder Icon", + "add_empty_icon": "Add Empty Icon" + }, + + "pack_settings": { + "label": "Pack Settings", + + "setting": { + "enabled": { + "label": "Enabled" + }, + + "corner_radius": { + "label": "Corner Radius", + "description": "Change the corner radius of the icons in this pack." + }, + + "opacity": { + "label": "Opacity", + "description": "Change the opacity of the icons in this pack." + } + } + }, + + "icons": { + "label": "Icons" + } + } + }, + + "settings": { + "restart_the_app_for_changes_to_take_effect": "Restart the app for changes to take effect.", + "are_you_sure_you_want_to_import_this_config": "Are you sure you want to import this config?", + "the_app_will_restart_to_load_the_new_config": "The app will be restarted to load the new config.", + "config_saved": "Config saved", + "there_was_an_error_saving_the_config": "There was an error saving the config.", + + "categories": { + "general": "General", + "application": "Application", + "icon_pack": "Icon Pack", + "system": "System", + "advanced": "Advanced", + "update": "Update" + }, + + "setting": { + "language": { + "label": "Language", + "description": "Select your preferred language.", + "select_language": "Select language...", + "search_language": "Search language...", + "no_languages_found": "No languages found." + }, + + "theme": { + "label": "Theme", + "description": "Choose a color scheme for the interface." + }, + + "color_scheme": { + "label": "Color Scheme", + "description": "Select your preferred color scheme.", + "select_color_scheme": "Select color scheme...", + "search_color_scheme": "Search color scheme...", + "no_color_schemes_found": "No color schemes found.", + + "color_schemes": { + "default": "Default", + "midnightAsh": "Midnight Ash", + "dawnMist": "Dawn Mist", + "forestDawn": "Forest Dawn", + "goldenEmber": "Golden Ember" + } + }, + + "window_effect": { + "label": "Window Effect", + "description": "Select the window effect.", + + "auto": "Auto", + "none": "None", + "acrylic": "Acrylic", + "mica": "Mica", + "tabbed": "Tabbed" + }, + + "window_opacity": { + "label": "Window Opacity", + "description": "Adjust the window opacity. (This setting is ignored if 'None' is selected for window effect.)" + }, + + "window_scale": { + "label": "Window Scale", + "description": "Adjust the window scale." + }, + + "use_system_title_bar": { + "label": "Use System Title Bar", + "description": "Use the default system title bar instead of a custom one." + }, + + "logging": { + "label": "Enable Logging", + "description": "Enable logging to files." + }, + + "log_levels": { + "label": "Log Levels", + "description": "Select the log levels to record." + }, + + "max_log_files": { + "label": "Max Log Files", + "description": "Set the maximum number of log files to retain." + }, + + "import_export": { + "label": "Import/Export Settings", + "description": "Import or export settings your settings from/to a JSON file." + }, + + "save_window_status": { + "label": "Save Window Status", + "description": "Save window size, position, and state." + }, + + "match_lnk_by_destination": { + "label": "Match Windows Shortuts (.lnk) by Destination", + "description": "Match windows shortcuts by destination if matching by path is not successful." + }, + + "match_url_by_destination": { + "label": "Match .url files by URL", + "description": "Match .url files by URL if matching by path is not successful." + }, + + "rename_matched_files": { + "label": "Rename Matched Files", + "description": "Rename matched files if the names does not match." + }, + + "change_description_of_matched_lnk_files": { + "label": "Change Description of Matched Windows Shortcut (.lnk) Files", + "description": "Change the description of matched shortucts." + }, + + "check_for_updates": { + "label": "Check For Updates On Startup", + "description": "Check for updates on startup." + }, + + "update": { + "update_available": "Update available", + "no_updates_available": "No updates available", + "last_checked": "Last checked", + "check_for_updates": "Check for updates", + "update": "Update", + "updating": "Updating...", + "failed_to_check_for_updates": "Failed to check for updates. Please try again later.", + "failed_to_download_update": "Failed to download update. Please try again later.", + "failed_to_apply_update": "Failed to apply update.", + "update_applied": "Update applied successfully.", + "restarting": "Restarting...", + "need_admin_privileges": "Administration privileges are needed to update the application.", + "update_successful": "Update was successfully applied." + } + } + } +} diff --git a/frontend/public/locales/tr-TR.json b/frontend/public/locales/tr-TR.json new file mode 100644 index 0000000..22a26fb --- /dev/null +++ b/frontend/public/locales/tr-TR.json @@ -0,0 +1,330 @@ +{ + "save": "Kaydet", + "cancel": "İptal", + "yes": "Evet", + "no": "Hayır", + "show_in_explorer": "Klasörü aç", + "import": "İçe Aktar", + "export": "Dışa Aktar", + "error": "Hata", + "show": "Göster", + "create": "Oluştur", + "delete": "Sil", + + "downloading_missing_external_programs": "Eksik dosyalar indiriliyor...", + "downloaded_missing_external_programs": "Eksik dosyalar başarıyla indirildi.", + + "nav": { + "my_packs": "Paketlerim", + "settings": "Ayarlar" + }, + + "file_info": { + "name": { + "label": "Ad", + "placeholder": "Dosya adı", + "help": "Bu, zaten bu ada sahip olmayan kısayollara verilecek yeni isimdir." + }, + + "description": { + "label": "Açıklama", + "placeholder": "Dosya açıklaması", + "help": "Bu, kısayola verilecek açıklamadır." + }, + + "path": { + "label": "Dosya yolu", + "placeholder": "Dosya yolu", + "help": "Bu, simgenin uygulanacağı dosyanın yoludur.", + "rules": "" + }, + + "destination": { + "label": "Hedef dosya yolu", + "placeholder": "Hedef dosya yolu", + "help": "Bu, dosyanın orijinal yolda bulunmaması durumunda, kaynak klasöründeki kısayolların hedef yollarıyla eşleştirilecek yoldur." + }, + + "url": { + "label": "URL", + "placeholder": "URL", + "help": "Bu, dosyanın orijinal yolda bulunmaması durumunda, kaynak klasöründeki dosyaların URL'leriyle eşleştirilecek URL'dir." + } + }, + + "my_packs": { + "create_new_pack": { + "title": "Simge Paketi Oluştur" + }, + + "delete_pack": { + "confirmation_title": "Bu paketi silmek istediğinizden emin misiniz?", + "confirmation_description": "Bu işlem geri alınamaz.", + "delete_generated_icons": "Oluşturulan simgeleri sil" + }, + + "import_pack": { + "confirmation_title": "Bu paketi yüklemek istiyor musunuz?", + "success": "Paket başarıyla yüklendi." + }, + + "edit_pack": { + "path_found": "Yol bulundu" + }, + + "buttons": { + "add_pack": { + "tooltip": "Yeni paket ekle" + }, + + "reload_packs": { + "tooltip": "Paketleri yenile" + }, + + "import_pack": { + "tooltip": "Paket yükle" + } + }, + + "card": { + "pack_information": { + "label": "Paket Bilgileri", + + "information": { + "name": { + "label": "Ad", + "placeholder": "Benim Paketim", + + "message": { + "name_required": "Ad gereklidir", + "name_max": "Ad, 32 karakteri geçemez" + } + }, + + "version": { + "label": "Versiyon", + "placeholder": "v1.0.0", + + "message": { + "version_required": "Versiyon gereklidir", + "version_format": "Versiyon, \"v1.0.0\" formatında olmalıdır" + } + }, + + "author": { + "label": "Yazar", + "placeholder": "Adınız", + + "message": { + "author_max": "Yazar, 32 karakteri geçemez" + } + }, + + "license": { + "label": "Lisans", + "placeholder": "MIT", + + "message": { + "license_max": "Lisans, 64 karakteri geçemez" + } + }, + + "description": { + "label": "Açıklama", + "placeholder": "Paket içeriği", + + "message": { + "description_max": "Açıklama, 1024 karakteri geçemez" + } + }, + + "icon": { + "label": "Simge" + } + } + }, + + "pack_actions": { + "label": "Paket İşlemleri", + "admin_warning": "Bazı simgeler yalnızca uygulama yönetici olarak çalıştırıldığında uygulanır.", + "restart_as_admin": "Yönetici olarak yeniden başlat", + "apply_icon_pack": "Uygula", + "export_icon_pack": "Dışa Aktar", + "edit_icon_pack": "Düzenle", + "add_icons_from_desktop": "Masaüstü Simgelerini Ekle", + "add_icons": "Simge Ekle", + "add_folder": "Klasör Simgesi Ekle", + "add_empty_icon": "Boş Simge Ekle" + }, + + "pack_settings": { + "label": "Paket Ayarları", + + "setting": { + "enabled": { + "label": "Etkin" + }, + + "corner_radius": { + "label": "Köşe Yarıçapı", + "description": "Bu paketteki simgelerin köşe yarıçapını değiştirin." + }, + + "opacity": { + "label": "Opaklık", + "description": "Bu paketteki simgelerin opaklığını değiştirin." + } + } + }, + + "icons": { + "label": "Simgeler" + } + } + }, + + "settings": { + "restart_the_app_for_changes_to_take_effect": "Değişikliklerin etkili olması için uygulamayı yeniden başlatın.", + "are_you_sure_you_want_to_import_this_config": "Bu ayarları yüklemek istediğinize emin misiniz?", + "the_app_will_restart_to_load_the_new_config": "Uygulama, yeni ayarları yüklemek için yeniden başlatılacak.", + "config_saved": "Ayarlar kaydedildi", + "there_was_an_error_saving_the_config": "Ayarlar kaydedilirken bir hata oluştu.", + + "categories": { + "general": "Genel", + "application": "Uygulama", + "icon_pack": "Simge Paketi", + "system": "Sistem", + "advanced": "Gelişmiş", + "update": "Güncelleme" + }, + + "setting": { + "language": { + "label": "Dil", + "description": "Tercih ettiğiniz dili seçin.", + "select_language": "Dil seçin...", + "search_language": "Dil ara...", + "no_languages_found": "Hiçbir dil bulunamadı." + }, + + "theme": { + "label": "Tema", + "description": "Arayüz için bir tema seçin." + }, + + "color_scheme": { + "label": "Renk Şeması", + "description": "Tercih ettiğiniz renk şemasını seçin.", + "select_color_scheme": "Renk şeması seçin...", + "search_color_scheme": "Renk şeması ara...", + "no_color_schemes_found": "Renk şeması bulunamadı.", + + "color_schemes": { + "default": "Varsayılan", + "midnightAsh": "Gece Külü", + "dawnMist": "Şafak Sisi", + "forestDawn": "Orman Şafağı", + "goldenEmber": "Altın Kor" + } + }, + + "window_effect": { + "label": "Pencere Efekti", + "description": "Pencere efektini seçin.", + + "auto": "Otomatik", + "none": "Yok", + "acrylic": "Akrilik", + "mica": "Mika", + "tabbed": "Tabbed" + }, + + "window_opacity": { + "label": "Pencere Opaklığı", + "description": "Pencere opaklığını ayarlayın. (Pencere efekti için 'Yok' seçilmişse bu ayar yok sayılır.)" + }, + + "window_scale": { + "label": "Pencere Ölçeği", + "description": "Pencere ölçeğini ayarlayın." + }, + + "use_system_title_bar": { + "label": "Sistem Başlık Çubuğunu Kullan", + "description": "Özel başlık çubuğu yerine varsayılan sistem başlık çubuğunu kullanın." + }, + + "logging": { + "label": "Günlüğe Kaydetmeyi Etkinleştir", + "description": "Dosyalara günlüğe kaydetmeyi etkinleştir." + }, + + "log_levels": { + "label": "Günlük Seviyeleri", + "description": "Kaydedilecek günlük seviyelerini seçin." + }, + + "max_log_files": { + "label": "Maksimum Günlük Dosyaları", + "description": "Tutulacak maksimum günlük dosyası sayısını ayarlayın." + }, + + "import_export": { + "label": "Ayarları İçe/Dışa Aktar", + "description": "Ayarlarınızı bir JSON dosyasından içe veya dışa aktarın." + }, + + "save_window_status": { + "label": "Pencere Durumunu Kaydet", + "description": "Pencere boyutunu, konumunu ve durumunu kaydet." + }, + + "match_by_destination": { + "label": "Kısayolları Hedeflerine Göre Eşleştir", + "description": "Dosya yolu ile eşleştirme başarısız olursa, kısayolları hedeflerine göre eşleştir." + }, + + "match_lnk_by_destination": { + "label": "Windows Kısayollarını (.lnk) Hedeflerine Göre Eşleştir", + "description": "Dosya yolu ile eşleştirme başarısız olursa, kısayolları hedeflerine göre eşleştir." + }, + + "match_url_by_destination": { + "label": "URL dosyalarını URL'e Göre Eşleştir", + "description": "Dosya yolu ile eşleştirme başarısız olursa, dosyaları URL'lerine göre eşleştir." + }, + + "rename_matched_files": { + "label": "Eşleşen Dosyaları Yeniden Adlandır", + "description": "İsimler eşleşmiyorsa, eşleşen dosyaları yeniden adlandır." + }, + + "change_description_of_matched_lnk_files": { + "label": "Eşleşen Windows Kısayol (.lnk) Dosyalarının Açıklamasını Değiştir", + "description": "Eşleşen kısayoların açıklamasını değiştir." + }, + + "check_for_updates": { + "label": "Başlangıçta Güncellemeleri Kontrol Et", + "description": "Başlangıçta güncellemeleri kontrol et." + }, + + "update": { + "update_available": "Güncelleme mevcut", + "no_updates_available": "Güncelleme yok", + "last_checked": "Son kontrol", + "check_for_updates": "Güncellemeleri kontrol et", + "update": "Güncelle", + "updating": "Güncelleniyor...", + "failed_to_check_for_updates": "Güncellemeler kontrol edilemedi. Lütfen daha sonra tekrar deneyin.", + "failed_to_download_update": "Güncelleme indirilemedi. Lütfen daha sonra tekrar deneyin.", + "failed_to_apply_update": "Güncelleme uygulanamadı.", + "update_applied": "Güncelleme uygulandı.", + "restarting": "Yeniden başlatılıyor...", + "need_admin_privileges": "Uygulamayı güncellemek için yönetici yetkileri gereklidir.", + "update_successful": "Güncelleme başarıyla uygulandı." + } + } + } +} diff --git a/frontend/public/setlnkdesc.vbs b/frontend/public/scripts/setlnkdesc.vbs similarity index 100% rename from frontend/public/setlnkdesc.vbs rename to frontend/public/scripts/setlnkdesc.vbs diff --git a/frontend/public/setlnkicon.vbs b/frontend/public/scripts/setlnkicon.vbs similarity index 87% rename from frontend/public/setlnkicon.vbs rename to frontend/public/scripts/setlnkicon.vbs index ae84591..b5cf8a9 100644 --- a/frontend/public/setlnkicon.vbs +++ b/frontend/public/scripts/setlnkicon.vbs @@ -1,4 +1,4 @@ -Dim Arg, shortcutPath, shortcutName, iconPath +Dim Arg, shortcutPath, shortcutName, iconPath, iconIndex Set Arg = WScript.Arguments shortcutPath = Arg(0) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1c4280d..4804b49 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,16 +1,164 @@ -import { ThemeProvider } from "./contexts/theme-provider"; -import { ProfileProvider } from "./contexts/profile-provider"; -import TopBar from "./components/TopBar"; -import { FileGrid } from "./components/FileGrid"; +import ModeToggle from "@/components/ModeToggle"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import TitleBar from "./components/TitleBar"; +import Settings from "./components/Settings"; +import { useTranslation } from "react-i18next"; +import { useEffect, useLayoutEffect, useState } from "react"; +import { useStorage } from "./contexts/storage-provider"; +import { Toaster } from "./components/ui/sonner"; +import { toast } from "sonner"; +import { + OpenFileInExplorer, + SendWindowsNotification, +} from "@/wailsjs/go/main/App"; +import React from "react"; +import { useConfig } from "./contexts/config-provider"; +import { LogDebug } from "@/wailsjs/runtime/runtime"; +import Packs from "./components/Packs"; +import { Progress } from "./components/ui/progress"; +import { useProgress } from "./contexts/progress-provider"; function App() { + const { config, initialConfig } = useConfig(); + const { t } = useTranslation(); + const { setValue, getValue } = useStorage(); + const [tab, setTab] = useState("packs"); + + const { progress, setProgress } = useProgress(); + + useLayoutEffect(() => { + if ( + config && + initialConfig && + config.windowScale !== undefined && + config.opacity !== undefined && + initialConfig.windowEffect !== undefined + ) { + document.documentElement.style.fontSize = + config.windowScale * (16 / 100) + "px"; + + document.documentElement.style.setProperty( + "--opacity", + ( + (initialConfig.windowEffect === 1 ? 100 : config.opacity) / 100 + ).toString() + ); + } + }, [config?.windowScale, config?.opacity, initialConfig?.windowEffect]); + + window.toast = ({ title, description, path, variant }: any) => { + const props = { + description: t(description), + action: path + ? { + label: path.startsWith("__") ? t("show") : t("show_in_explorer"), + onClick: () => handleToastGotoPath(path), + } + : undefined, + }; + switch (variant) { + case "message": + toast.message(t(title), props); + break; + case "success": + toast.success(t(title), props); + break; + case "info": + toast.info(t(title), props); + break; + case "warning": + toast.warning(t(title), props); + break; + case "error": + toast.error(t(title), props); + break; + default: + toast(t(title), props); + break; + } + }; + + const handleToastGotoPath = (path: string) => { + if (path.startsWith("__")) { + window.goto(path.substring(2)); + } else { + OpenFileInExplorer(path); + } + }; + + window.goto = (path: string) => { + LogDebug("window.goto: " + path); + const pathArray = path.split("__"); + + setTab(pathArray[0]); + + for (let i = 0; i < pathArray.length - 1; i++) { + setValue(pathArray[i], pathArray[i + 1]); + } + }; + + useEffect(() => { + setValue("path1", tab); + }, [tab]); + + window.sendNotification = ( + title: string, + message: string, + path: string, + variant: string + ) => { + SendWindowsNotification(t(title), t(message), path, variant); + }; + + window.setProgress = (value: number) => { + setProgress(value); + }; + return ( - - - - - - + +
+ + +
+ +
+ setTab("packs")} + disabled={getValue("editingIconPack")} + className="px-6" + > + {t("nav.my_packs")} + + setTab("settings")} + disabled={getValue("editingIconPack")} + className="px-6" + > + {t("nav.settings")} + +
+ +
+
+ +
+
+ + + + + + + +
+
+ +
); } diff --git a/frontend/src/assets/appicon.png b/frontend/src/assets/appicon.png new file mode 100644 index 0000000..84bcdc2 Binary files /dev/null and b/frontend/src/assets/appicon.png differ diff --git a/frontend/src/assets/folder-search.png b/frontend/src/assets/folder-search.png deleted file mode 100644 index 66524d9..0000000 Binary files a/frontend/src/assets/folder-search.png and /dev/null differ diff --git a/frontend/src/assets/folder-search.svg b/frontend/src/assets/folder-search.svg deleted file mode 100644 index 0de6ce9..0000000 --- a/frontend/src/assets/folder-search.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/frontend/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/assets/unknown.png b/frontend/src/assets/unknown.png deleted file mode 100644 index a886939..0000000 Binary files a/frontend/src/assets/unknown.png and /dev/null differ diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/frontend/src/assets/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/colorSchemes.json b/frontend/src/colorSchemes.json new file mode 100644 index 0000000..1580a3e --- /dev/null +++ b/frontend/src/colorSchemes.json @@ -0,0 +1,24 @@ +{ + "colorSchemes": [ + { + "code": "default", + "authors": ["Bedirhan Yenilmez"] + }, + { + "code": "midnightAsh", + "authors": ["Bedirhan Yenilmez"] + }, + { + "code": "dawnMist", + "authors": ["Bedirhan Yenilmez"] + }, + { + "code": "forestDawn", + "authors": ["Bedirhan Yenilmez"] + }, + { + "code": "goldenEmber", + "authors": ["Bedirhan Yenilmez"] + } + ] +} diff --git a/frontend/src/components/CreateProfileForm.tsx b/frontend/src/components/CreateProfileForm.tsx deleted file mode 100644 index bf6862c..0000000 --- a/frontend/src/components/CreateProfileForm.tsx +++ /dev/null @@ -1,76 +0,0 @@ -"use client" - -import { AddProfile } from "wailsjs/go/main/App"; - -import { zodResolver } from "@hookform/resolvers/zod" -import { useForm } from "react-hook-form" -import { z } from "zod" - -import { Button } from "@/components/ui/button" -import { - Form, - FormControl, - /* FormDescription, */ - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form" -import { Input } from "@/components/ui/input" - -const AddProfileF = (name: string) => { - AddProfile(name).then(() => { - console.log("Profile created: " + name); - // select profile - }); -} - -const formSchema = z.object({ - profileName: z.string() - .min(1, { message: "Profile name is required" }) - .max(50, { message: "Profile name can not exceed 50 characters" }) - // Check for windows file name compatibility - .regex(/^(?!\.)/, { message: "Profile name can not start with a period" }) - .regex(/^(?!.*\.$)/, { message: "Profile name can not end with a period" }) - .regex(/^[^<>:"/\\|?*]*$/, { message: "Profile name can not contain any of the following characters: < > : \" / \\ | ? *" }) - .regex(/^(?!con$)(?!prn$)(?!aux$)(?!nul$)(?!com[0-9]$)(?!lpt[0-9]$)(?!com[¹²³])(?!lpt[¹²³])/i, { message: "Profile name can not be any of the following: con, prn, aux, nul, com[0-9], lpt[0-9], com⁽¹⁻³⁾, lpt⁽¹⁻³⁾" }) - , -}) - -export function CreateProfileForm() { - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - profileName: "", - } - }); - - function onSubmit(data: z.infer) { - AddProfileF(data.profileName); - } - - return ( -
- - ( - - Profile name - - - - {/* - This is the name of your profile - */} - - - )} - /> - - - - ) - -} \ No newline at end of file diff --git a/frontend/src/components/FileContainer.tsx b/frontend/src/components/FileContainer.tsx deleted file mode 100644 index 0098258..0000000 --- a/frontend/src/components/FileContainer.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { useEffect, useRef, useState } from "react" -import { GetIcon, SaveIcon, SaveProfile } from "wailsjs/go/main/App" -import { Input } from "@/components/ui/input" -import { Pencil, Settings2, XOctagon } from "lucide-react" -import { Button } from "./ui/button" -import { useProfile } from "@/contexts/profile-provider" - -import unknown from "../assets/folder-search.svg" -import { fileInfo } from "@/structs" - - -interface FileContainerProps { - fileInfo: fileInfo - index: number -} - -export const FileContainer = (props: FileContainerProps) => { - const { profile, setProfile } = useProfile(); - const [editing, setEditing] = useState(false); - - const iconRef = useRef(null); - - useEffect(() => { - GetIconF(); - }, []) - - useEffect(() => { - if (props.fileInfo.iconName !== "") { - GetIconF(); - } else { - iconRef.current!.src = unknown - } - }, [props.fileInfo.iconName]) - - useEffect(() => { - if (editing) { - SaveProfile(profile.name, JSON.stringify(profile)).then(() => setEditing(false)) - } - }, [profile]) - - function GetIconF() { - if (props.fileInfo.iconName !== "") { - GetIcon(profile.name, props.fileInfo.iconName).then((res) => { - if (res) iconRef.current!.src = res - }); - } - } - - function DeleteRow() { - setEditing(true) - setProfile({ ...profile, value: profile.value.filter((_, i) => i !== props.index) }) - } - - return ( -
-
- - { - setEditing(true) - setProfile({ ...profile, value: profile.value.map((f, i) => i === props.index ? { ...f, name: e.target.value } : f) }) - }} - /> - { - setEditing(true); - setProfile({ ...profile, value: profile.value.map((f, i) => i === props.index ? { ...f, description: e.target.value } : f) }) - }} /> -
-
- - -
-
- ) -} \ No newline at end of file diff --git a/frontend/src/components/FileGrid.tsx b/frontend/src/components/FileGrid.tsx deleted file mode 100644 index 2aaa9d5..0000000 --- a/frontend/src/components/FileGrid.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { useEffect, useState } from "react"; -import { useProfile } from "@/contexts/profile-provider"; -import { SaveProfile, GetFileInfo } from "wailsjs/go/main/App"; -import { FileContainer } from "./FileContainer" - -import { fileInfo } from "@/structs" -import { Button } from "./ui/button"; -import { PlusSquare } from "lucide-react"; - -export const FileGrid = () => { - const { profile, setProfile } = useProfile(); - const [fileInfos, setFileInfos] = useState(); - - /* const GetDesktopIconsF = () => { - GetDesktopIcons().then((res) => { - console.log(res); - setFileInfos(res); - }) - } - - useEffect(() => { - GetDesktopIconsF(); - }, []) */ - - useEffect(() => { - setFileInfos(profile?.value); - }, [profile]) - - const AddRow = () => { - GetFileInfo(profile.name).then((fileInfo) => { - if (fileInfo.extension !== ".lnk") return - setProfile({ ...profile, value: [...profile.value, fileInfo] }) - SaveProfile(profile.name, JSON.stringify({ ...profile, value: [...profile.value, fileInfo] })) - }) - } - - - return ( -
- {fileInfos?.map((fileInfo, i) => ( - - ))} -
- {profile.name && ( - - )} -
- -
- ) -} \ No newline at end of file diff --git a/frontend/src/components/Image.tsx b/frontend/src/components/Image.tsx new file mode 100644 index 0000000..50055de --- /dev/null +++ b/frontend/src/components/Image.tsx @@ -0,0 +1,45 @@ +import React, { useState } from "react"; +import { Skeleton } from "./ui/skeleton"; +import { CircleHelp } from "lucide-react"; + +// Define the props type for the Image component +interface ImageProps { + src: string; // Assuming `icon` is a URL or path to the image + className?: string; + cornerRadius?: number; + opacity?: number; + unkown?: boolean; +} + +const Image: React.FC = ({ + src, + className, + cornerRadius = 0, + opacity = 100, + unkown = false, + ...rest +}) => { + const [loading, setLoading] = useState(true); + + return ( + <> + {unkown ? : + + } + { + setLoading(false) + }} + style={{ + borderRadius: `${cornerRadius}%`, + opacity: `${opacity / 100}`, + }} + {...rest} + /> + + ); +}; + +export default Image; diff --git a/frontend/src/components/ModeToggle.tsx b/frontend/src/components/ModeToggle.tsx new file mode 100644 index 0000000..4f07345 --- /dev/null +++ b/frontend/src/components/ModeToggle.tsx @@ -0,0 +1,23 @@ +import { Moon, Sun } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { useTheme } from "@/contexts/theme-provider"; + +export default function ModeToggle() { + const { theme, setTheme } = useTheme(); + + return ( + + ); +} diff --git a/frontend/src/components/Packs.tsx b/frontend/src/components/Packs.tsx new file mode 100644 index 0000000..b98caad --- /dev/null +++ b/frontend/src/components/Packs.tsx @@ -0,0 +1,1758 @@ +"use client"; + +import { useState, useEffect, useRef, useCallback } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Switch } from "@/components/ui/switch"; +import { + Check, + CircleAlert, + Download, + Edit, + Folder, + FolderOpen, + Images, + Loader2, + LucideTrash, + Monitor, + Pencil, + Plus, + RefreshCw, + SquarePlus, + Trash, + UploadIcon, +} from "lucide-react"; +import { + AreYouSureDialog, + AreYouSureDialogRef, +} from "@/components/ui/are-you-sure"; +import { + Dialog, + DialogClose, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import SelectImage from "./SelectImage"; +import { + AddDeletePngRelativePath, + AddFilesToIconPackFromDesktop, + AddFilesToIconPackFromPath, + AddFileToIconPackFromPath, + AddIconPack, + ApplyIconPack, + ClearDeletePngPaths, + ClearIconPackCache, + ClearSelectImages, + ClearTempPngPaths, + CreateLastTab, + DeleteDeletePngPaths, + DeleteIconPack, + Description, + Destination, + ExportIconPack, + Ext, + GeneralPathExits, + GetFileInfoFromDesktop, + GetFileInfoFromPaths, + GetFilePath, + GetIcnmMetadata, + GetIconFiles, + GetIconFolder, + GetIconPack, + GetIconPackList, + GetIconPackPath, + ImportIconPack, + Name, + NeedsAdminPrivileges, + ReadLastTab, + RestartApplication, + SetIconPackField, + SetIconPackFiles, + SetIconPackMetadata, + SetImageIfAbsent, + UUID, +} from "@/wailsjs/go/main/App"; +import { main } from "@/wailsjs/go/models"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { + SettingContent, + SettingDescription, + SettingLabel, + SettingsItem, +} from "./ui/settings-group"; +import Image from "./Image"; +import { Slider } from "./ui/my-slider"; +import { Skeleton } from "./ui/skeleton"; +import { Checkbox } from "./ui/checkbox"; +import { useTranslation } from "react-i18next"; +import { useStorage } from "@/contexts/storage-provider"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { LogDebug } from "@/wailsjs/runtime/runtime"; +import { Label } from "./ui/label"; +import { Textarea } from "./ui/textarea"; +import { HoverCard, HoverCardContent, HoverCardTrigger } from "./ui/hover-card"; +import { HelpCard } from "./ui/help-card"; +import parse from "html-react-parser"; +import { useProgress } from "@/contexts/progress-provider"; + +export default function Packs() { + const { t } = useTranslation(); + const { getValue, setValue } = useStorage(); + + const [editingIconPack, setEditingIconPack] = useState(false); + useEffect(() => { + setValue("editingIconPack", editingIconPack); + }, [editingIconPack]); + + const [selectedPackId, setSelectedPackId] = useState(""); + const [iconPacks, setIconPacks] = useState(); + const [selectedPackKeyCount, setSelectedPackKeyCount] = useState(0); + + const [reloadingIconPacks, setReloadingIconPacks] = useState(false); + + const dialogCloseRef = useRef(null); + const dialogImportRef = useRef(null); + const [importPackPath, setImportPackPath] = useState(""); + + const [tempMetadata, setTempMetadata] = useState( + main.Metadata.createFrom({}) + ); + + const tabsListRef = useRef(null); + const [hasOverflow, setHasOverflow] = useState(false); + useEffect(() => { + const element = tabsListRef.current as HTMLElement | null; + if (!element) { + return; + } + + const overflow = element.scrollHeight > element.clientHeight; + LogDebug( + element.scrollHeight + " > " + element.clientHeight + " = " + overflow + ); + setHasOverflow(overflow); + }, [editingIconPack]); + + useEffect(() => { + setSelectedPackId(getValue("packs") || ""); + }, [getValue("packs")]); + + const loadIconPacks = async () => { + const packs = await GetIconPackList(); + setIconPacks(packs); + }; + + const reloadSelectedPack = () => { + setSelectedPackKeyCount(selectedPackKeyCount + 1); + }; + + const handleReloadIconPacks = async () => { + setReloadingIconPacks(true); + + // Start the timer for at least 250ms + const spinMinDuration = new Promise((resolve) => setTimeout(resolve, 250)); + + // Clear icon cache and reload packs + const reloadJob = ClearIconPackCache().then(() => { + return loadIconPacks().then(() => { + reloadSelectedPack(); + }); + }); + + // Wait for both the spin duration and the job to complete + await Promise.all([spinMinDuration, reloadJob]); + + // Stop the spin animation + setReloadingIconPacks(false); + }; + + const handleImportIconPack = () => { + GetIconPackPath().then((path) => { + if (path) { + setImportPackPath(path); + GetIcnmMetadata(path).then((metadata) => { + setTempMetadata(metadata); + dialogImportRef.current?.openDialog(); + }); + } else { + setImportPackPath(""); + } + }); + }; + + window.importIconPack = (path: string) => { + setImportPackPath(path); + GetIcnmMetadata(path).then((metadata) => { + if (metadata.id === "") { + setImportPackPath(""); + } else { + setTempMetadata(metadata); + dialogImportRef.current?.openDialog(); + } + }); + }; + + const handleAcceptImportIconPack = () => { + ImportIconPack(importPackPath).then((id) => { + handleReloadIconPacks().then(() => { + ClearTempPngPaths(); + setSelectedPackId(id); + }); + }); + }; + + useEffect(() => { + loadIconPacks(); + }, []); + + useEffect(() => { + reloadSelectedPack(); + }, [selectedPackId]); + + useEffect(() => { + if (!editingIconPack && selectedPackId !== "") { + CreateLastTab(selectedPackId).then(() => { + window.location.reload(); + }); + } + }, [editingIconPack]); + + useEffect(() => { + ReadLastTab().then((packId) => { + if (packId) { + setSelectedPackId(packId); + } + }); + }, []); + + return ( + +
+
+
+ + + + + + + + + {t("my_packs.create_new_pack.title")} + + + + + + +
+
+ { + setImportPackPath(""); + ClearTempPngPaths(); + }} + > +
+ +
+
+ {tempMetadata.name} +
+
+ {tempMetadata.version} +
+
+
+
+ +
+
+ + + {iconPacks?.map((pack) => ( + + ))} + +
+ + {selectedPackId && + (editingIconPack ? ( + + ) : ( + + ))} +
+ ); +} + +interface PackTriggerProps { + packId: string; + selectedPackId: string; + setSelectedPackId: (packId: string) => void; + reloadSelectedPack: () => void; + editingIconPack: boolean; + disabled?: boolean; + loadIconPacks: () => void; +} + +function PackTrigger({ + packId, + selectedPackId, + setSelectedPackId, + reloadSelectedPack, + editingIconPack, + disabled, + loadIconPacks, + ...props +}: PackTriggerProps) { + const [iconPack, setIconPack] = useState(); + const [enabledState, setEnabledState] = useState(false); + + useEffect(() => { + GetIconPack(packId).then((iconPack) => { + setIconPack(iconPack); + setEnabledState(iconPack.settings.enabled); + }); + }, [packId]); + + const handleEnable = () => { + SetIconPackField(packId, "settings.json", "enabled", !enabledState).then( + () => { + setEnabledState(!enabledState); + loadIconPacks(); + if (packId === selectedPackId) { + reloadSelectedPack(); + } + } + ); + }; + + return ( + setSelectedPackId(packId)} + className="flex justify-between p-4 w-full" + disabled={disabled} + {...props} + > +
+ + {!editingIconPack && ( +
+
+ {iconPack?.metadata.name} +
+
{iconPack?.metadata.version}
+
+ )} +
+ {false && !editingIconPack && ( + e.stopPropagation()} + /> + )} +
+ ); +} + +interface PackContentProps { + iconPackId: string; + setSelectedPackId: (packId: string) => void; + loadIconPacks: () => void; + setEditingIconPack: (editingIconPack: boolean) => void; + reloadIconPacks: () => void; +} + +function PackContent({ + iconPackId, + setSelectedPackId, + loadIconPacks, + setEditingIconPack, + reloadIconPacks, +}: PackContentProps) { + const { t } = useTranslation(); + const { progress } = useProgress(); + + const [needsAdmin, setNeedsAdmin] = useState(false); + + const [loading, setLoading] = useState(true); + const [editingMetadata, setEditingMetadata] = useState(false); + const [iconPack, setIconPack] = useState(); + const dialogRef = useRef(null); + const [deleteGeneratedIcons, setDeleteGeneratedIcons] = useState(false); + + const [enabled, setEnabled] = useState(false); + const [cornerRadius, setCornerRadius] = useState(-1); + const [opacity, setOpacity] = useState(-1); + + const [applyRunning, setApplyRunning] = useState(false); + useState(false); + const [addIconsFromDesktopRunning, setAddIconsFromDesktopRunning] = + useState(false); + const [addIconsRunning, setAddIconsRunning] = useState(false); + const [addFolderRunning, setAddFolderRunning] = useState(false); + const running = + applyRunning || + addIconsFromDesktopRunning || + addIconsRunning || + addFolderRunning || + editingMetadata || + progress != 0; + + useEffect(() => { + GetIconPack(iconPackId).then((pack) => { + setIconPack(pack); + setEnabled(pack.settings.enabled); + setOpacity(pack.settings.opacity); + setCornerRadius(pack.settings.cornerRadius); + setLoading(false); + }); + + NeedsAdminPrivileges().then((result) => { + setNeedsAdmin(result); + }); + }, []); + + const handleSettingChange = ( + field: keyof main.IconPack["settings"], + value: boolean | number + ) => { + if (iconPack === undefined) { + return; + } + + SetIconPackField(iconPackId, "settings.json", field, value).then(() => { + const newIconPack = { + ...iconPack, + settings: { + ...iconPack.settings, + [field]: value, + }, + } as main.IconPack; + + setIconPack(newIconPack); + + if (field === "enabled") { + console.log("loadIconPacks"); + loadIconPacks(); + } + }); + }; + + const handleEditStart = () => { + setEditingMetadata(true); + }; + + const handleEditSave = (metadata: main.Metadata) => { + const updateMetadataJob = async () => { + if (iconPack === undefined) { + return; + } + + const oldMetadata = iconPack.metadata; + + oldMetadata.name = metadata.name; + oldMetadata.version = metadata.version; + oldMetadata.author = metadata.author; + oldMetadata.license = metadata.license; + oldMetadata.description = metadata.description; + + SetIconPackMetadata(iconPackId, oldMetadata); + }; + + Promise.all([ + ClearSelectImages(), + DeleteDeletePngPaths(), + updateMetadataJob(), + ]).then(() => { + reloadIconPacks(); + }); + }; + + const handleEditCancel = () => { + setEditingMetadata(false); + ClearTempPngPaths(); + ClearDeletePngPaths(); + ClearSelectImages(); + }; + + const handleDelete = () => { + DeleteIconPack(iconPackId, deleteGeneratedIcons).then(() => { + setSelectedPackId(""); + loadIconPacks(); + }); + }; + + const fields: (keyof main.IconPack["metadata"])[] = [ + "name", + "version", + "author", + "license", + ]; + + const handleEditIconPack = () => { + setEditingIconPack(true); + }; + + const handleExportIconPack = () => { + ExportIconPack(iconPackId); + }; + + const handleApplyIconPack = () => { + setApplyRunning(true); + ApplyIconPack(iconPackId).finally(() => { + setApplyRunning(false); + }); + }; + + const handleAddIconsFromDesktop = () => { + setAddIconsFromDesktopRunning(true); + + AddFilesToIconPackFromDesktop(iconPackId).then(() => { + GetIconPack(iconPackId) + .then((pack) => { + setIconPack(pack); + }) + .finally(() => { + setAddIconsFromDesktopRunning(false); + }); + }); + }; + + const handleAddIcon = () => { + GetIconFiles().then((files) => { + if (files) { + setAddIconsRunning(true); + + AddFilesToIconPackFromPath(iconPackId, files, true).then(() => { + GetIconPack(iconPackId) + .then((pack) => { + setIconPack(pack); + }) + .finally(() => { + setAddIconsRunning(false); + }); + }); + } + }); + }; + + const handleAddFolder = () => { + GetIconFolder().then((folder) => { + if (folder) { + setAddFolderRunning(true); + + AddFileToIconPackFromPath(iconPackId, folder, true).then(() => { + GetIconPack(iconPackId) + .then((pack) => { + setIconPack(pack); + }) + .finally(() => { + setAddFolderRunning(false); + }); + }); + } + }); + }; + + const openDialog = useCallback(() => { + if (dialogRef.current) { + setDeleteGeneratedIcons(false); + dialogRef.current.openDialog(); + } + }, []); + + if (loading || iconPack === undefined) { + return ( +
+ + + + +
+ ); + } + + return ( + +
+
+ {t("my_packs.card.pack_information.label")} +
+
+
+
+
+ {editingMetadata && ( + + )} + +
+ + {!editingMetadata && ( +
+
+ {fields.map((field) => ( +
+
+ {t( + "my_packs.card.pack_information.information." + + field + + ".label" + )} +
+
+ {iconPack.metadata[field]} +
+
+ ))} +
+ {iconPack.metadata.description && ( +
+
+ {t( + "my_packs.card.pack_information.information.description.label" + )} +
+
+ {iconPack.metadata.description} +
+
+ )} +
+ )} + + {editingMetadata && ( + + )} +
+
+ {!editingMetadata && ( +
+ + + +
+ + setDeleteGeneratedIcons(!deleteGeneratedIcons) + } + /> + +
+
+
+ )} +
+
+ +
+
+ {t("my_packs.card.pack_actions.label")} + + {needsAdmin && ( +
+ +
+ + {t("my_packs.card.pack_actions.admin_warning")} +
+
+ )} +
+
+ + + + + +
+ +
+ + + + +
+
+ +
+
+ {t("my_packs.card.pack_settings.label")} +
+ {false && ( + + + {t("my_packs.card.pack_settings.setting.enabled.label")} + + + { + setEnabled(enabled as boolean); + handleSettingChange("enabled", enabled); + }} + /> + + + )} + + +
+ + {t("my_packs.card.pack_settings.setting.corner_radius.label")} + + + {t( + "my_packs.card.pack_settings.setting.corner_radius.description" + )} + +
+ +
+
0%
+ setCornerRadius(value[0] as number)} + onPointerUp={() => + handleSettingChange("cornerRadius", cornerRadius) + } + defaultValue={[iconPack.settings.cornerRadius]} + min={0} + max={50} + step={1} + className={"w-56 cursor-pointer"} + /> +
50%
+
+ ({cornerRadius}%) +
+
+
+
+ + +
+ + {t("my_packs.card.pack_settings.setting.opacity.label")} + + + {t("my_packs.card.pack_settings.setting.opacity.description")} + +
+ +
+
10%
+ setOpacity(value[0] as number)} + onPointerUp={() => handleSettingChange("opacity", opacity)} + defaultValue={[iconPack.settings.opacity]} + min={10} + max={100} + step={1} + className={"w-56 cursor-pointer"} + /> +
100%
+
({opacity}%)
+
+
+
+
+ + {iconPack.files?.filter((file) => file.hasIcon).length > 0 && ( +
+
+ {t("my_packs.card.icons.label")} +
+
+ {iconPack.files?.map((file) => + file.hasIcon ? ( + + ) : null + )} +
+
+ )} +
+ ); +} + +interface PackEditProps { + iconPackId: string; + setEditingIconPack: (editingIconPack: boolean) => void; +} + +function PackEdit({ iconPackId, setEditingIconPack }: PackEditProps) { + const { t } = useTranslation(); + const { progress } = useProgress(); + + const [loading, setLoading] = useState(true); + const [files, setFiles] = useState(); + const [updateArray, setUpdateArray] = useState( + Array.from({ length: 4096 }, (_, i) => i) + ); + const [updateArray2, setUpdateArray2] = useState( + Array.from({ length: 4096 }, (_, i) => i) + ); + + const [addIconsFromDesktopRunning, setAddIconsFromDesktopRunning] = + useState(false); + const [addIconsRunning, setAddIconsRunning] = useState(false); + const [addFolderRunning, setAddFolderRunning] = useState(false); + const running = + addIconsFromDesktopRunning || + addIconsRunning || + addFolderRunning || + progress != 0; + + useEffect(() => { + GetIconPack(iconPackId).then((pack) => { + setFiles(pack.files); + setLoading(false); + }); + }, []); + + const handleAddIconsFromDesktop = () => { + setAddIconsFromDesktopRunning(true); + + GetFileInfoFromDesktop("temp") + .then((fileInfos) => { + const oldFiles = files || []; + oldFiles.push(...fileInfos); + setFiles(oldFiles); + }) + .finally(() => { + setAddIconsFromDesktopRunning(false); + }); + }; + + const handleAddIcon = () => { + GetIconFiles().then((paths) => { + if (paths) { + setAddIconsRunning(true); + + GetFileInfoFromPaths("temp", paths) + .then((fileInfos) => { + const oldFiles = files || []; + oldFiles.push(...fileInfos); + setFiles(oldFiles); + }) + .finally(() => { + setAddIconsRunning(false); + }); + } + }); + }; + + const handleAddFolder = () => { + GetIconFolder().then((folder) => { + if (folder) { + setAddFolderRunning(true); + + GetFileInfoFromPaths("temp", [folder]) + .then((fileInfos) => { + const oldFiles = files || []; + oldFiles.push(...fileInfos); + setFiles(oldFiles); + }) + .finally(() => { + setAddFolderRunning(false); + }); + } + }); + }; + + const handleAddEmptyIcon = () => { + setAddIconsRunning(true); + + UUID() + .then((uuid) => { + const oldFiles = files || []; + oldFiles.push( + main.FileInfo.createFrom({ + id: uuid, + name: "New file", + description: "", + path: "", + destinationPath: "", + extension: "", + hasIcon: false, + iconId: "", + }) + ); + setFiles(oldFiles); + }) + .finally(() => { + setAddIconsRunning(false); + }); + }; + + const handleRemoveIcon = (index: number) => { + if (!files) return; + setFiles((prevFiles) => prevFiles?.filter((_, i) => i !== index)); + AddDeletePngRelativePath( + `packs\\${iconPackId}\\icons\\${files[index].id}.png` + ); + }; + + const handleInputChange = (index: number, field: string, value: string) => { + setFiles((prevFiles) => + prevFiles?.map((file, i) => + i === index ? { ...file, [field]: value } : file + ) + ); + }; + + const handleCancelEdit = () => { + ClearTempPngPaths(); + ClearSelectImages().then(() => setEditingIconPack(false)); + }; + + const handleSaveEdit = () => { + if (!files) return; + DeleteDeletePngPaths().then(() => { + SetIconPackFiles(iconPackId, files).then(() => { + ClearTempPngPaths(); + ClearSelectImages().then(() => setEditingIconPack(false)); + }); + }); + }; + + if (loading || files === undefined) { + return ( +
+ + + + + + + + + + + +
+ ); + } + + return ( +
+
+
+ + + + +
+
+ + +
+
+
+ + {files.map((file, index) => ( + + { + e.stopPropagation(); + handleRemoveIcon(index); + }} + > + + + } + > +
+ + {file.name} +
+
+ +
+
+
+ + { + setUpdateArray((prevUpdateArray) => { + const newArray = [...prevUpdateArray]; + newArray[index] = prevUpdateArray[index] + 1; + return newArray; + }); + }} + key={updateArray2[index]} + editable + /> +
+ { + handleInputChange(index, "name", value); + }} + label={t("file_info.name.label")} + className="justify-between w-full" + /> +
+ {file.extension === ".lnk" && ( + { + handleInputChange(index, "description", value); + }} + label={t("file_info.description.label")} + /> + )} + { + handleInputChange(index, "path", value); + Ext(value).then((ext) => { + handleInputChange(index, "extension", ext); + console.log(ext); + }); + if (file.name === "" || file.name === "New file") { + Name(value).then((name) => { + handleInputChange(index, "name", name); + }); + } + if (file.description === "") { + Description(value).then((description) => { + console.log(description); + handleInputChange(index, "description", description); + }); + } + if (file.destinationPath === "") { + Destination(value).then((destinationPath) => { + console.log("d: " + destinationPath); + handleInputChange( + index, + "destinationPath", + destinationPath + ); + }); + } + + SetImageIfAbsent(file.id, value).then(() => { + setUpdateArray((prevUpdateArray) => { + const newArray = [...prevUpdateArray]; + newArray[index] = prevUpdateArray[index] + 1; + return newArray; + }); + setUpdateArray2((prevUpdateArray) => { + const newArray = [...prevUpdateArray]; + newArray[index] = prevUpdateArray[index] + 1; + return newArray; + }); + }); + }} + label={t("file_info.path.label")} + /> + + {(file.extension === ".lnk" || file.extension === ".url") && ( + { + handleInputChange(index, "destinationPath", value); + }} + label={t( + `file_info.${ + file.extension === ".url" ? "url" : "destination" + }.label` + )} + /> + )} +
+
+
+ ))} +
+
+
+ ); +} + +function TextInput({ + value, + placeholder, + onChange, + label, + className = "", + helpText, +}: { + value: string; + placeholder?: string; + onChange: (value: string) => void; + label: string; + className?: string; + helpText?: string; +}) { + return ( +
+
+ + {helpText && } +
+ { + onChange(e.target.value); + }} + /> +
+ ); +} + +function PathInput({ + value, + placeholder, + onChange, + label, + helpText, +}: { + value: string; + placeholder?: string; + onChange: (value: string) => void; + label: string; + helpText?: string; +}) { + const { t } = useTranslation(); + const [exists, setExists] = useState(false); + + const handleChoosePath = () => { + GetFilePath(value).then((path) => { + if (path) { + LogDebug("Path selected: " + path); + onChange(path); + } + }); + }; + + const updateExistence = () => { + GeneralPathExits(value).then((exists) => { + setExists(exists); + }); + }; + + useEffect(() => { + updateExistence(); + }, [value]); + + return ( +
+
+ { + onChange(value); + updateExistence(); + }} + label={label} + className="w-full" + helpText={helpText!} + /> + {exists && ( + + + + + + {t("my_packs.edit_pack.path_found")} + + + )} +
+ +
+ ); +} + +interface CreatePackFormProps { + reloadIconPacks?: () => void; + dialogCloseRef?: React.RefObject; + handleSave?: (metadata: main.Metadata) => void; + handleCancel?: () => void; + defaultValues?: main.Metadata; +} + +function CreatePackForm({ + reloadIconPacks, + dialogCloseRef, + handleSave, + handleCancel, + defaultValues, +}: CreatePackFormProps) { + const { t } = useTranslation(); + + const formSchema = z.object({ + icon: z.string(), + name: z + .string() + .min(1, { + message: t( + "my_packs.card.pack_information.information.name.message.name_required" + ), + }) + .max(32, { + message: t( + "my_packs.card.pack_information.information.name.message.name_max" + ), + }), + version: z + .string() + .min(1, { + message: t( + "my_packs.card.pack_information.information.version.message.version_required" + ), + }) + .regex(/^v[0-9]{1,4}\.[0-9]{1,4}\.[0-9]{1,4}$/, { + message: t( + "my_packs.card.pack_information.information.version.message.version_format" + ), + }), + author: z.string().max(32, { + message: t( + "my_packs.card.pack_information.information.author.message.author_max" + ), + }), + license: z.string().max(64, { + message: t( + "my_packs.card.pack_information.information.license.message.license_max" + ), + }), + description: z.string().max(1024, { + message: t( + "my_packs.card.pack_information.information.description.message.description_max" + ), + }), + }); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + icon: "", + name: defaultValues?.name || "", + version: defaultValues?.version || "v1.0.0", + author: defaultValues?.author || "", + license: defaultValues?.license || "", + description: defaultValues?.description || "", + }, + }); + + function onSubmit(data: z.infer) { + if (reloadIconPacks) { + AddIconPack( + data.name, + data.version, + data.author, + data.license, + data.description + ).then(() => { + reloadIconPacks(); + dialogCloseRef?.current?.click(); + }); + } else { + handleSave?.(main.Metadata.createFrom(data)); + } + } + + useEffect(() => { + return () => { + // This function runs before the component unmounts + ClearTempPngPaths(); + ClearSelectImages(); + }; + }, []); + + return ( +
+ + {!defaultValues && ( + ( + + + {t("my_packs.card.pack_information.information.icon.label")} + + + + + )} + /> + )} +
+
+ ( + + + {t("my_packs.card.pack_information.information.name.label")} + + + + + + + )} + /> + ( + + + {t( + "my_packs.card.pack_information.information.version.label" + )} + + + + + + + )} + /> + ( + + + {t( + "my_packs.card.pack_information.information.author.label" + )} + + + + + + + )} + /> + ( + + + {t( + "my_packs.card.pack_information.information.license.label" + )} + + + + + + + )} + /> +
+
+ ( + + + {t( + "my_packs.card.pack_information.information.description.label" + )} + + +