feat!: add Config Swap plugin (#112)
* feat!: init plugin * fix: fix widget not found on bar setting * feat!: add creation folder for plugin * feat!: add first version of Config Swap (simple modification test) * feat!: update toml with new feature * docs: prepare docs * feat!: add more place for view preview * feat!: add new page for store and allerady download * docs: improve lisibility * feat!: add description emplacement * feat!: add wallpaper support for install and set * docs: add more info to readme * feat: add cp depedencie * docs: add more info * feat!: add trad support * fix: change order for set wallpaper after change config * feat!: add settingsPath for data.json * feat!: add data.json creation if not exist * docs: add more docs info * feat!: rm useless preview image * docs: improve docs * feat!: change false description * feat!: add shellQuote for protect bad command * feat!: add path var in infoConfig.Change config type for infoConfig Type * feat!: add secure delete config and confirmation for delete config * feat!: add save function for save current config * feat!: add welcome warning and confirmation switch * feat!: add info for store section * docs: add new info in setting section * feat!: add resolve state dir for check all state dir possibility * feat!: add english translation compatibility * feat!: change thumbnail * docs: add more information on the readme * fix: rm useless button * docs: add comment * fix: add depedencie missing * fix: rm death code * fix: rm update interval not use * feat!: add delete, save and apply protection, reject invalid name * docs: add more info in info section * feat!: add more charactere in match string in isDetect function * fix: change wrong name function * feat!: add protection for invalid name in dowload Config
This commit is contained in:
@@ -0,0 +1,130 @@
|
|||||||
|
# Config Swap
|
||||||
|
|
||||||
|
A [Noctalia](https://github.com/noctalia-dev/noctalia) v5 plugin for switching between saved Noctalia configurations.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
For this plugin, you need:
|
||||||
|
* internet access to fetch configuration data from GitHub on the store page
|
||||||
|
* `cp` to apply a configuration
|
||||||
|
* `rm` to delete the selected installed configuration (not the current configuration)
|
||||||
|
|
||||||
|
## Plugin
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | `tadomika_ari/config-swap` |
|
||||||
|
| Entries | Bar widget: `widget`; panel: `panel`; service: `start` |
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
This plugin is still under development.
|
||||||
|
|
||||||
|
You can open Config Swap with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
noctalia msg panel-toggle tadomika_ari/config-swap:panel
|
||||||
|
```
|
||||||
|
|
||||||
|
The plugin downloads saved configurations from the `Config-Swap-Box` GitHub repository and stores them in `~/Config-Swap`.
|
||||||
|
Applying a configuration copies its `settings.toml` to `~/.local/state/noctalia/settings.toml` and sets the matching wallpaper.
|
||||||
|
Keep a backup of your current configuration before using it.
|
||||||
|
|
||||||
|
Open the panel from the bar widget, then switch between the two views with `Show Store` and `Show List`.
|
||||||
|
|
||||||
|
|
||||||
|
### Before Start
|
||||||
|
|
||||||
|
A welcome page is shown after each restart. Agree to continue, or close the plugin if you prefer not to use it.
|
||||||
|
|
||||||
|
### Show Store
|
||||||
|
|
||||||
|
The store view fetches available configurations from GitHub.
|
||||||
|
Use `refresh` to reload the list if nothing appears.
|
||||||
|
|
||||||
|
Do not spam `refresh`; GitHub rate limits unauthenticated requests.
|
||||||
|
|
||||||
|
Each card lets you `install` a configuration.
|
||||||
|
|
||||||
|
An information panel is also available to explain the store view.
|
||||||
|
|
||||||
|
| Action | Effect |
|
||||||
|
| --- | --- |
|
||||||
|
| Refresh | Refresh the GitHub API data |
|
||||||
|
| Info | Show information about the store |
|
||||||
|
| Install | Install the configuration in `~/Config-Swap` |
|
||||||
|
| Show list | Switch to the list view |
|
||||||
|
|
||||||
|
### Show List
|
||||||
|
|
||||||
|
The list view shows configurations already installed in `~/Config-Swap`.
|
||||||
|
From there, you can `apply` them again or `delete` them again to refresh the files.
|
||||||
|
You will be asked to confirm before applying the selected configuration and confirm before deleting the selected configuration.
|
||||||
|
|
||||||
|
Click a preview image to see extra information such as the author, origin, and description.
|
||||||
|
|
||||||
|
Use `Show Store` to return to the GitHub store view.
|
||||||
|
|
||||||
|
| Action | Effect |
|
||||||
|
| --- | --- |
|
||||||
|
| Refresh | Refresh the GitHub API data |
|
||||||
|
| Delete | Delete the configuration in `~/Config-Swap` |
|
||||||
|
| Apply | Apply the configuration |
|
||||||
|
| Setting | Open the Config Swap settings |
|
||||||
|
| Click the preview | Open extra information |
|
||||||
|
| Show store | Switch to the store view |
|
||||||
|
|
||||||
|
### Setting Info
|
||||||
|
|
||||||
|
A settings panel is available. At this time, only saving a configuration is supported.
|
||||||
|
|
||||||
|
You can save your current configuration under a custom name for backup or export.
|
||||||
|
Saving a configuration copies the current `settings.toml` and creates an `info.json` file, which is important for the plugin and for export to the GitHub store. The `info.json` file can be edited to add information, and you can also add a `preview.png` and a default wallpaper as `wallpaper.png`.
|
||||||
|
Files are saved in `~/Config-Swap/{name}`.
|
||||||
|
|
||||||
|
This also refreshes the list of installed configurations.
|
||||||
|
|
||||||
|
| Action | Effect |
|
||||||
|
| --- | --- |
|
||||||
|
| Input field | Enter a custom name for the save (default: `save`) |
|
||||||
|
| Save | Create a backup of the current configuration |
|
||||||
|
|
||||||
|
|
||||||
|
### Extra Info
|
||||||
|
|
||||||
|
You can add your own configuration folder to `~/Config-Swap`.
|
||||||
|
Each configuration should follow the same structure as the downloaded ones, including an `info.json` file and the expected asset files.
|
||||||
|
|
||||||
|
If you want to publish a configuration, add it to the `Config-Swap-Box` repository: https://github.com/Tadomika-Ari/Config-Swap-Box
|
||||||
|
Make sure Config Swap is enabled in your plugin list before you try to use it.
|
||||||
|
|
||||||
|
You can delete a configuration with the trash button. A warning panel will appear before deletion.
|
||||||
|
|
||||||
|
### Contributing
|
||||||
|
|
||||||
|
You can contribute to Config Swap! Add your own configuration files and wallpaper to the store so others can use them.
|
||||||
|
To do so:
|
||||||
|
* Go to the Settings section and save your configuration. The plugin will copy your `settings.toml` and create an `info.json` file.
|
||||||
|
* Take a preview image and wallpaper, then go to `~/Config-Swap/{name}` and copy your `preview.png` and `wallpaper.png` files (the exact names are required).
|
||||||
|
* Update your `info.json` with important information such as the author and description.
|
||||||
|
* Go to the GitHub page linked from the information panel in the Store section and create a pull request.
|
||||||
|
* Wait for review, and then your configuration can be shared.
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
|
||||||
|
No setting needed
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Noctalia ≥ 5.0.0
|
||||||
|
- `cp`
|
||||||
|
- `rm`
|
||||||
|
- internet access for the store view
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
Install the plugin and add it to your bar.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT.
|
||||||
@@ -0,0 +1,655 @@
|
|||||||
|
--!nonstrict
|
||||||
|
-- Config Swap panel
|
||||||
|
|
||||||
|
-- Local variables
|
||||||
|
|
||||||
|
local configBox = "~/Config-Swap"
|
||||||
|
|
||||||
|
local listConfig = nil
|
||||||
|
|
||||||
|
local pickSlots = {}
|
||||||
|
local columns = 2
|
||||||
|
local previewWidth = 470
|
||||||
|
local previewHeight = 350
|
||||||
|
local previewImage = ""
|
||||||
|
|
||||||
|
local selectedView = 0 -- Controls which screen to render (0=welcome, 1=installed, 2=store)
|
||||||
|
|
||||||
|
local previewCache = {} -- In-memory map: config name -> cached preview image path
|
||||||
|
local previewPending = {}
|
||||||
|
|
||||||
|
local configWidth = 800 -- Preview dialog size
|
||||||
|
local configHeight = 600
|
||||||
|
|
||||||
|
-- Data model
|
||||||
|
|
||||||
|
type infoConfig = {
|
||||||
|
name: string, -- Configuration name
|
||||||
|
author: string, -- Configuration author
|
||||||
|
origin: string, -- Distribution/source (NixOS, Arch, CachyOS, etc.)
|
||||||
|
preview: string, -- Preview image path (preview.png)
|
||||||
|
path: string, -- Config path (example: config/{name})
|
||||||
|
description: string, -- Short description
|
||||||
|
wallpaperPath: string, -- Wallpaper path
|
||||||
|
depedencie: boolean, -- Whether extra dependencies are required
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Resolve Noctalia state directory
|
||||||
|
|
||||||
|
local function resolveStateDir()
|
||||||
|
return noctalia.getenv("NOCTALIA_STATE_HOME")
|
||||||
|
or ((noctalia.getenv("XDG_STATE_HOME") or ((noctalia.getenv("HOME") or "") .. "/.local/state")) .. "/noctalia")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Detecte dangerous charactère
|
||||||
|
|
||||||
|
function isDetect(target: string)
|
||||||
|
local match_string = "[/.$();]"
|
||||||
|
|
||||||
|
if string.match(target, match_string) then
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Store listing cache
|
||||||
|
|
||||||
|
local allConfig: { infoConfig } = {}
|
||||||
|
|
||||||
|
local requestFetchList = {
|
||||||
|
url = "https://api.github.com/repos/Tadomika-Ari/Config-Swap-Box/contents/config",
|
||||||
|
method = "GET",
|
||||||
|
headers = { "Accept: application/json", "User-Agent: Config-Swap-Box-Plugin" },
|
||||||
|
follow_redirects = true
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Translation helper
|
||||||
|
|
||||||
|
function tr(key, subst)
|
||||||
|
if subst then
|
||||||
|
return noctalia.tr("panel." .. key, subst)
|
||||||
|
end
|
||||||
|
return noctalia.tr("panel." .. key)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Build settings.toml path from the resolved state dir
|
||||||
|
|
||||||
|
function takePosSetting()
|
||||||
|
local noctaliaStateDir = resolveStateDir()
|
||||||
|
local settingsTomlPath = noctaliaStateDir .. "/settings.toml"
|
||||||
|
return settingsTomlPath
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Shell escaping to safely build external commands
|
||||||
|
|
||||||
|
local function shellQuote(s)
|
||||||
|
return "'" .. tostring(s):gsub("'", "'\\''") .. "'"
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- Welcome warning screen
|
||||||
|
|
||||||
|
local function welcomeWarning() -- Shown after startup; refusing closes the plugin
|
||||||
|
panel.render(ui.column({ flexGrow = 1, gap = 16 }, {
|
||||||
|
ui.row({ align = "center", justify = "space_between", gap = 8 }, {
|
||||||
|
ui.label({ text = tr("title"), textAlign = "center", fontSize = 16, fontWeight = "bold", color = "on_surface", flexGrow = 1 }),
|
||||||
|
}),
|
||||||
|
ui.label({ text = tr("welcome_message_title"), textAlign = "center", fontSize = 16, fontWeight = "bold", color = "on_surface" }),
|
||||||
|
ui.label({ text = tr("welcome_message1"), textAlign = "center", fontSize = 14, color = "on_surface_variant" }),
|
||||||
|
ui.label({ text = tr("welcome_message2"), textAlign = "center", fontSize = 14, color = "on_surface_variant" }),
|
||||||
|
ui.row({ gap = 8, justify = "center" }, {
|
||||||
|
ui.button({ text = tr("not_agree"), variant = "ghost", onClick = "onCloseClicked" }),
|
||||||
|
ui.button({ text = tr("agree"), onClick = function()
|
||||||
|
selectedView = 1
|
||||||
|
render()
|
||||||
|
end
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function renderConfirmApply(data: infoConfig) -- Confirmation dialog before applying a config
|
||||||
|
panel.render(ui.column({ flexGrow = 1, gap = 16 }, {
|
||||||
|
ui.row({ align = "center", justify = "space_between", gap = 8 }, {
|
||||||
|
ui.label({ text = tr("title"), textAlign = "center", fontSize = 16, fontWeight = "bold", color = "on_surface", flexGrow = 1 }),
|
||||||
|
}),
|
||||||
|
ui.label({ text = tr("confirmation"), textAlign = "center", fontSize = 16, fontWeight = "bold", color = "on_surface" }),
|
||||||
|
ui.label({ text = data.name, textAlign = "center", fontSize = 14, color = "on_surface_variant" }),
|
||||||
|
ui.label({ text = tr("description_confirmation"), textAlign = "center", fontSize = 13, color = "on_surface_variant" }),
|
||||||
|
ui.row({ gap = 8, justify = "center" }, {
|
||||||
|
ui.button({ text = tr("cancel"), variant = "ghost", onClick = "render" }),
|
||||||
|
ui.button({ text = tr("apply"), variant = "primary", onClick = function()
|
||||||
|
applyConfig(data)
|
||||||
|
render()
|
||||||
|
end
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Apply selected config and wallpaper
|
||||||
|
|
||||||
|
function applyConfig(data: infoConfig)
|
||||||
|
noctalia.notify(tr("apply_notification_title"))
|
||||||
|
|
||||||
|
if isDetect(data.name) == true then -- detecte malicious name
|
||||||
|
noctalia.notify(tr("apply_notification_title"), "Invalid name. Please check your info.json")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local pathSettings = takePosSetting()
|
||||||
|
if not pathSettings then
|
||||||
|
noctalia.notify(tr("notification_title"), "Path not found, please check your setting toml")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local src = shellQuote(noctalia.expandPath(configBox .. "/" .. data.name .. "/settings.toml"))
|
||||||
|
local dest = shellQuote(noctalia.expandPath(pathSettings))
|
||||||
|
local cmd = "cp " .. src .. " " .. dest
|
||||||
|
|
||||||
|
noctalia.runAsync(cmd, function(result)
|
||||||
|
if result and result.exitCode == 0 then
|
||||||
|
noctalia.setWallpaper(configBox .. "/" .. data.name .. "/wallpaper.png")
|
||||||
|
else
|
||||||
|
noctalia.notify(tr("notification_title"), "swap failed")
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Download the full config bundle: settings, info json, preview, wallpaper
|
||||||
|
|
||||||
|
function downloadConfig(data: infoConfig)
|
||||||
|
noctalia.notify(tr("install_notification_title"))
|
||||||
|
|
||||||
|
if isDetect(data.name) == true then
|
||||||
|
noctalia.notify("Download", "Invalide name detected, please check target config")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
noctalia.mkdirAll(configBox .. "/" .. data.name)
|
||||||
|
|
||||||
|
local path = data.path .. "/settings.toml"
|
||||||
|
local url = "https://raw.githubusercontent.com/Tadomika-Ari/Config-Swap-Box/main/" .. path
|
||||||
|
noctalia.download(url, configBox .. "/" .. data.name .. "/settings.toml" , function(ok)
|
||||||
|
if ok then
|
||||||
|
noctalia.notify(tr("notification_title"), tr("download_ok"))
|
||||||
|
else
|
||||||
|
noctalia.notify(tr("notification_title"), tr("download_failed"))
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
local path = data.path .. "/info.json"
|
||||||
|
local url = "https://raw.githubusercontent.com/Tadomika-Ari/Config-Swap-Box/main/" .. path
|
||||||
|
noctalia.download(url, configBox .. "/" .. data.name .. "/info.json" , function(ok)
|
||||||
|
if ok then
|
||||||
|
noctalia.notify(tr("notification_title"), tr("download_ok"))
|
||||||
|
else
|
||||||
|
noctalia.notify(tr("notification_title"), tr("download_failed"))
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
local path = data.path .. "/preview.png"
|
||||||
|
local url = "https://raw.githubusercontent.com/Tadomika-Ari/Config-Swap-Box/main/" .. path
|
||||||
|
noctalia.download(url, configBox .. "/" .. data.name .. "/preview.png" , function(ok)
|
||||||
|
if ok then
|
||||||
|
noctalia.notify(tr("notification_title"), tr("download_ok"))
|
||||||
|
else
|
||||||
|
noctalia.notify(tr("notification_title"), tr("download_failed"))
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
local path = data.path .. "/wallpaper.png"
|
||||||
|
local url = "https://raw.githubusercontent.com/Tadomika-Ari/Config-Swap-Box/main/" .. path
|
||||||
|
noctalia.download(url, configBox .. "/" .. data.name .. "/wallpaper.png" , function(ok)
|
||||||
|
if ok then
|
||||||
|
noctalia.notify(tr("notification_title"), tr("download_ok"))
|
||||||
|
else
|
||||||
|
noctalia.notify(tr("notification_title"), tr("download_failed"))
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- GitHub store preview cache
|
||||||
|
|
||||||
|
local function previewCachePath(name)
|
||||||
|
-- Keep cached previews under plugin data to avoid repeated downloads.
|
||||||
|
local dir = noctalia.pluginDataDir() .. "/preview-cache"
|
||||||
|
noctalia.mkdirAll(dir)
|
||||||
|
return dir .. "/" .. name .. ".png"
|
||||||
|
end
|
||||||
|
|
||||||
|
local function queuePreviewDownload(name)
|
||||||
|
-- Prevent duplicate requests for the same preview while one is in flight.
|
||||||
|
if previewCache[name] ~= nil or previewPending[name] then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local dest = previewCachePath(name)
|
||||||
|
if noctalia.fileExists(dest) then
|
||||||
|
previewCache[name] = dest
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
previewPending[name] = true
|
||||||
|
local url = "https://raw.githubusercontent.com/Tadomika-Ari/Config-Swap-Box/main/config/" .. name .. "/preview.png"
|
||||||
|
|
||||||
|
noctalia.download(url, dest, function(ok)
|
||||||
|
previewPending[name] = nil
|
||||||
|
if ok then
|
||||||
|
previewCache[name] = dest
|
||||||
|
render()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Delete an installed config
|
||||||
|
|
||||||
|
local function delete(name)
|
||||||
|
|
||||||
|
if not name or name == "" then
|
||||||
|
noctalia.notify("Delete", "invalid name")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if isDetect(name) == true then -- detecte malicious name
|
||||||
|
noctalia.notify("Delete", "Invalide Name. Please check info.json")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local target = noctalia.expandPath(configBox .. "/" .. name)
|
||||||
|
local cmd = "rm -rf " .. shellQuote(target) -- shellQuote protects the rm target path
|
||||||
|
noctalia.runAsync(cmd, function(result)
|
||||||
|
if result and result.exitCode == 0 then
|
||||||
|
noctalia.notify("Delete Ok")
|
||||||
|
else
|
||||||
|
noctalia.notify("Delete failed")
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
render()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function renderDeleteConfirmation(name) -- Confirmation panel before deleting a config
|
||||||
|
panel.render(ui.column({ flexGrow = 1, gap = 16 }, {
|
||||||
|
ui.label({ text = tr("confirmation_delete") .. name .. "?", align = "center", fontSize = 16, fontWeight = "bold", color = "on_surface", flexGrow = 1 }),
|
||||||
|
ui.button({ text = tr("delete"), onClick = function()
|
||||||
|
delete(name)
|
||||||
|
end
|
||||||
|
}),
|
||||||
|
ui.button({ glyph = "close", onClick = "render" }),
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Save/backup settings section
|
||||||
|
|
||||||
|
local noctaliaStateDir = resolveStateDir()
|
||||||
|
local settingsTomlPath = noctaliaStateDir .. "/settings.toml" -- Active settings file path
|
||||||
|
|
||||||
|
local saveNameInput = "save"
|
||||||
|
|
||||||
|
local function saveConfig()
|
||||||
|
local name = saveNameInput ~= "" and saveNameInput or "save"
|
||||||
|
|
||||||
|
if isDetect(name) then -- detecte special charactere
|
||||||
|
noctalia.notify("Save", "Invalid Name")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local savePath = configBox .. "/" .. name
|
||||||
|
noctalia.mkdirAll(savePath)
|
||||||
|
|
||||||
|
local settingFile: infoConfig = {
|
||||||
|
name = name,
|
||||||
|
description = "save for reset",
|
||||||
|
path = configBox .. "/" .. name,
|
||||||
|
origin = "not give",
|
||||||
|
wallpaperPath = nil,
|
||||||
|
depedencie = false,
|
||||||
|
preview = nil,
|
||||||
|
author = noctalia.getenv("USER"),
|
||||||
|
}
|
||||||
|
local jsonData = noctalia.json.encode(settingFile)
|
||||||
|
noctalia.writeFile(savePath .. "/info.json", jsonData)
|
||||||
|
|
||||||
|
local content = noctalia.readFile(settingsTomlPath)
|
||||||
|
if content then
|
||||||
|
noctalia.writeFile(savePath .. "/settings.toml", content)
|
||||||
|
noctalia.notify("Save", "Save ok : " .. name)
|
||||||
|
render()
|
||||||
|
else
|
||||||
|
noctalia.notify("Save", "settings.toml not found")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function onSaveNameChange(value)
|
||||||
|
saveNameInput = value
|
||||||
|
end
|
||||||
|
|
||||||
|
local function renderSetting()
|
||||||
|
local rowsSettings = {
|
||||||
|
ui.column({ gap = 8 }, {
|
||||||
|
ui.label({
|
||||||
|
text = tr("description_setting1"),
|
||||||
|
fontSize = 12,
|
||||||
|
color = "on_surface_variant",
|
||||||
|
}),
|
||||||
|
ui.row({ flexGrow = 1, gap = 16, align = "center" }, {
|
||||||
|
ui.input({
|
||||||
|
key = "save-name-input",
|
||||||
|
value = saveNameInput,
|
||||||
|
placeholder = "Save name",
|
||||||
|
onChange = "onSaveNameChange",
|
||||||
|
flexGrow = 1,
|
||||||
|
}),
|
||||||
|
ui.button({ text = tr("save"), onClick = function()
|
||||||
|
saveConfig()
|
||||||
|
end
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
panel.render(ui.column({ flexGrow = 1, gap = 16 }, {
|
||||||
|
ui.label({ text = "Setting section", textAlign = "center", fontSize = 16, fontWeight = "bold", color = "on_surface", flexGrow = 1 }),
|
||||||
|
ui.button({ text = "X", onClick = "render"}),
|
||||||
|
ui.scroll({ gap = 12, align = "stretch", flexGrow = 1 }, rowsSettings),
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Store information panel
|
||||||
|
|
||||||
|
local function renderInfoStore()
|
||||||
|
panel.render(ui.column({ flexGrow = 1, gap = 16 }, {
|
||||||
|
ui.row({ align = "center", justify = "space_between", gap = 8 }, {
|
||||||
|
ui.label({ text = tr("info_store_title"), textAlign = "center", fontSize = 16, fontWeight = "bold", color = "on_surface", flexGrow = 1 }),
|
||||||
|
ui.button({ text = tr("info_store_github_button"), onClick = function()
|
||||||
|
noctalia.copyToClipboard("https://github.com/Tadomika-Ari/Config-Swap-Box", "text/plain")
|
||||||
|
noctalia.notify(tr("notification_title"), tr("info_store_copy_link_success"))
|
||||||
|
end
|
||||||
|
}),
|
||||||
|
ui.button({ glyph = "close", onClick = "render" }),
|
||||||
|
}),
|
||||||
|
ui.scroll({ gap = 16, align = "stretch", flexGrow = 1 }, {
|
||||||
|
ui.column({ gap = 4 }, {
|
||||||
|
ui.label({ text = tr("info_store_what_is_title"), fontSize = 13, fontWeight = "bold", color = "on_surface" }),
|
||||||
|
ui.label({ text = tr("info_store_what_is_body"), fontSize = 12, color = "on_surface_variant" }),
|
||||||
|
}),
|
||||||
|
ui.column({ gap = 4 }, {
|
||||||
|
ui.label({ text = tr("info_store_installing_title"), fontSize = 13, fontWeight = "bold", color = "on_surface" }),
|
||||||
|
ui.label({ text = tr("info_store_installing_body"), fontSize = 12, color = "on_surface_variant" }),
|
||||||
|
}),
|
||||||
|
ui.column({ gap = 4 }, {
|
||||||
|
ui.label({ text = tr("info_store_applying_title"), fontSize = 13, fontWeight = "bold", color = "on_surface" }),
|
||||||
|
ui.label({ text = tr("info_store_applying_body"), fontSize = 12, color = "on_surface_variant" }),
|
||||||
|
}),
|
||||||
|
ui.column({ gap = 4 }, {
|
||||||
|
ui.label({ text = tr("info_store_community_title"), fontSize = 13, fontWeight = "bold", color = "on_surface" }),
|
||||||
|
ui.label({ text = tr("info_store_community_body"), fontSize = 12, color = "on_surface_variant" }),
|
||||||
|
}),
|
||||||
|
ui.column({ gap = 4 }, {
|
||||||
|
ui.label({ text = tr("info_how_to_submite_config_title"), fontSize = 13, fontWeight = "bold", color = "on_surface" }),
|
||||||
|
ui.label({ text = tr("info_how_to_submite_config_body"), fontSize = 12, color = "on_surface_variant" }),
|
||||||
|
}),
|
||||||
|
ui.column({ gap = 4 }, {
|
||||||
|
ui.label({ text = tr("info_store_rate_limits_title"), fontSize = 13, fontWeight = "bold", color = "on_surface" }),
|
||||||
|
ui.label({ text = tr("info_store_rate_limits_body"), fontSize = 12, color = "on_surface_variant" }),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function renderList()
|
||||||
|
|
||||||
|
pickSlots = {}
|
||||||
|
local tiles = {}
|
||||||
|
local list = listConfig or {}
|
||||||
|
|
||||||
|
for count = 1, #list, 1 do
|
||||||
|
local data = list[count]
|
||||||
|
local slotIndex = count - 1
|
||||||
|
pickSlots[slotIndex] = data.path
|
||||||
|
|
||||||
|
queuePreviewDownload(data.name)
|
||||||
|
|
||||||
|
local imagePath = previewCache[data.name] or previewImage
|
||||||
|
|
||||||
|
table.insert(tiles, ui.column({ gap = 4, key = data.path, align = "center" }, {
|
||||||
|
ui.image({
|
||||||
|
path = imagePath,
|
||||||
|
width = previewWidth,
|
||||||
|
height = previewHeight,
|
||||||
|
fit = "cover",
|
||||||
|
radius = 8,
|
||||||
|
onClick = ""
|
||||||
|
}),
|
||||||
|
ui.row( {gap = 12, align = "start"}, {
|
||||||
|
ui.button({ text = tr("install"), onClick = function()
|
||||||
|
downloadConfig(data)
|
||||||
|
end
|
||||||
|
}),
|
||||||
|
ui.label({
|
||||||
|
text = data.name,
|
||||||
|
fontSize = 11,
|
||||||
|
maxLines = 1,
|
||||||
|
maxWidth = 120,
|
||||||
|
textAlign = "center",
|
||||||
|
}),
|
||||||
|
} ),
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
local rows = {}
|
||||||
|
local row = {}
|
||||||
|
for i, tile in ipairs(tiles) do
|
||||||
|
table.insert(row, tile)
|
||||||
|
if #row == columns or i == #tiles then
|
||||||
|
table.insert(rows, ui.row({ gap = 12, align = "start" }, row))
|
||||||
|
row = {}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
panel.render(ui.column({ flexGrow = 1, gap = 16 }, {
|
||||||
|
ui.row({ align = "center", justify = "space_between", gap = 8 }, {
|
||||||
|
ui.label({ text = tr("title"), align = "center", fontSize = 16, fontWeight = "bold", color = "on_surface", flexGrow = 1 }),
|
||||||
|
ui.label({ text = tr("store_title"), align = "center", fontSize = 16, fontWeight = "bold", color = "on_surface", flexGrow = 1 }),
|
||||||
|
ui.button({ text = tr("show_list"), onClick = function()
|
||||||
|
selectedView = 1
|
||||||
|
render()
|
||||||
|
end
|
||||||
|
}),
|
||||||
|
ui.button({ text = "Info", onClick = function()
|
||||||
|
renderInfoStore()
|
||||||
|
end
|
||||||
|
}),
|
||||||
|
ui.button({ text = tr("refresh"), onClick = "getFetchList"}),
|
||||||
|
ui.button({ glyph = "close", onClick = "onCloseClicked" }),
|
||||||
|
}),
|
||||||
|
ui.scroll({ gap = 12, align = "stretch", flexGrow = 1 }, rows),
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Installed config details panel
|
||||||
|
|
||||||
|
local function renderConfig(data: infoConfig)
|
||||||
|
local rows = {
|
||||||
|
ui.image({
|
||||||
|
path = configBox .. "/" .. data.name .. "/preview.png",
|
||||||
|
width = configWidth,
|
||||||
|
height = configHeight,
|
||||||
|
fit = "cover",
|
||||||
|
radius = 8,
|
||||||
|
}),
|
||||||
|
ui.label({ text = data.name, align = "center", fontSize = 16, fontWeight = "bold", color = "on_surface", flexGrow = 1 }),
|
||||||
|
ui.label({ text = tr("created_by", { author = data.author }), align = "center", fontSize = 16, fontWeight = "bold", color = "on_surface", flexGrow = 1 }),
|
||||||
|
ui.label({ text = tr("origin", { origin = data.origin }), align = "center", fontSize = 16, fontWeight = "bold", color = "on_surface", flexGrow = 1 }),
|
||||||
|
ui.label({ text = tr("description", { description = data.description or "" }), align = "center", fontSize = 16, fontWeight = "bold", color = "on_surface", flexGrow = 1 }),
|
||||||
|
ui.button({ glyph = "trash", onClick = function()
|
||||||
|
renderDeleteConfirmation(data.name)
|
||||||
|
end
|
||||||
|
}),
|
||||||
|
ui.button({ glyph = "close", onClick = "render" }),
|
||||||
|
}
|
||||||
|
panel.render(ui.column({ flexGrow = 1, gap = 16 }, {
|
||||||
|
ui.scroll({ gap = 12, align = "stretch", flexGrow = 1 }, rows),
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- List installed configs from ~/Config-Swap
|
||||||
|
|
||||||
|
function getListInstall()
|
||||||
|
local localPath = configBox
|
||||||
|
local list = noctalia.listDir(localPath)
|
||||||
|
local listShow: { infoConfig } = {}
|
||||||
|
|
||||||
|
for i, entryName in ipairs(list) do
|
||||||
|
local infoPath = configBox .. "/" .. entryName .. "/info.json"
|
||||||
|
local content = noctalia.readFile(infoPath)
|
||||||
|
local data = content and noctalia.json.decode(content) or nil
|
||||||
|
|
||||||
|
if not data then
|
||||||
|
-- Fallback entry when info.json is missing or invalid.
|
||||||
|
table.insert(listShow, {
|
||||||
|
name = entryName,
|
||||||
|
path = "config/" .. entryName,
|
||||||
|
author = "None",
|
||||||
|
origin = "None",
|
||||||
|
preview = nil,
|
||||||
|
description = nil,
|
||||||
|
wallpaperPath = nil,
|
||||||
|
depedencie = false,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(listShow, {
|
||||||
|
name = data.name,
|
||||||
|
path = data.path,
|
||||||
|
author = data.author,
|
||||||
|
origin = data.origin,
|
||||||
|
preview = data.preview,
|
||||||
|
description = data.description,
|
||||||
|
wallpaperPath = data.wallpaperPath,
|
||||||
|
depedencie = data.depedencie,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
return listShow
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Render installed configs grid
|
||||||
|
|
||||||
|
local function renderInstall()
|
||||||
|
pickSlots = {}
|
||||||
|
local tiles = {}
|
||||||
|
local list = getListInstall() or {}
|
||||||
|
for count = 1, #list, 1 do
|
||||||
|
local data = list[count]
|
||||||
|
local slotIndex = count - 1
|
||||||
|
pickSlots[slotIndex] = data.path
|
||||||
|
table.insert(tiles, ui.column({ gap = 4, key = data.path, align = "center" }, {
|
||||||
|
ui.image({
|
||||||
|
path = configBox .. "/" .. data.name .. "/preview.png",
|
||||||
|
width = previewWidth,
|
||||||
|
height = previewHeight,
|
||||||
|
fit = "cover",
|
||||||
|
radius = 8,
|
||||||
|
onClick = function()
|
||||||
|
renderConfig(data)
|
||||||
|
end
|
||||||
|
}),
|
||||||
|
ui.row( {gap = 12, align = "start"}, {
|
||||||
|
ui.button({ text = "delete", onClick = function()
|
||||||
|
renderDeleteConfirmation(data.name)
|
||||||
|
end
|
||||||
|
}),
|
||||||
|
ui.label({
|
||||||
|
text = data.name,
|
||||||
|
fontSize = 11,
|
||||||
|
maxLines = 1,
|
||||||
|
maxWidth = 120,
|
||||||
|
textAlign = "center",
|
||||||
|
}),
|
||||||
|
ui.button({ text = "apply", onClick = function()
|
||||||
|
renderConfirmApply(data)
|
||||||
|
end
|
||||||
|
}),
|
||||||
|
} ),
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
local rows = {}
|
||||||
|
local row = {}
|
||||||
|
for i, tile in ipairs(tiles) do
|
||||||
|
table.insert(row, tile)
|
||||||
|
if #row == columns or i == #tiles then
|
||||||
|
table.insert(rows, ui.row({ gap = 12, align = "start" }, row))
|
||||||
|
row = {}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
panel.render(ui.column({ flexGrow = 1, gap = 16 }, {
|
||||||
|
ui.row({ align = "center", justify = "space_between", gap = 8 }, {
|
||||||
|
ui.label({ text = tr("title"), align = "center", fontSize = 16, fontWeight = "bold", color = "on_surface", flexGrow = 1 }),
|
||||||
|
ui.label({ text = tr("installed_title"), align = "center", fontSize = 16, fontWeight = "bold", color = "on_surface", flexGrow = 1 }),
|
||||||
|
ui.button({ text = tr("show_store"), onClick = function()
|
||||||
|
selectedView = 2
|
||||||
|
render()
|
||||||
|
end
|
||||||
|
}),
|
||||||
|
ui.button({ text = "Setting", onClick = function()
|
||||||
|
renderSetting()
|
||||||
|
end
|
||||||
|
}),
|
||||||
|
ui.button({ text = tr("refresh"), onClick = "getFetchList"}),
|
||||||
|
ui.button({ glyph = "close", onClick = "onCloseClicked" }),
|
||||||
|
}),
|
||||||
|
ui.scroll({ gap = 12, align = "stretch", flexGrow = 1 }, rows),
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Root view router
|
||||||
|
|
||||||
|
function render()
|
||||||
|
if selectedView == 0 then
|
||||||
|
welcomeWarning()
|
||||||
|
end
|
||||||
|
if selectedView == 1 then
|
||||||
|
renderInstall()
|
||||||
|
end
|
||||||
|
if selectedView == 2 then
|
||||||
|
renderList()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Fetch available configs from GitHub
|
||||||
|
|
||||||
|
function getFetchList(onDone)
|
||||||
|
allConfig = {}
|
||||||
|
noctalia.http(requestFetchList, function(respond)
|
||||||
|
noctalia.log("raw body: " .. tostring(respond.body))
|
||||||
|
if not respond.ok then
|
||||||
|
noctalia.notify("Error", respond.status)
|
||||||
|
end
|
||||||
|
local data, err = noctalia.json.decode(respond.body)
|
||||||
|
if err then
|
||||||
|
noctalia.log(err)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
for i, item in ipairs(data) do
|
||||||
|
table.insert(allConfig, {
|
||||||
|
name = item.name,
|
||||||
|
path = item.path,
|
||||||
|
author = nil,
|
||||||
|
depedencie = nil,
|
||||||
|
description = nil,
|
||||||
|
origin = nil,
|
||||||
|
preview = nil,
|
||||||
|
wallpaperPath = nil,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
listConfig = allConfig
|
||||||
|
render()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Plugin entry points
|
||||||
|
|
||||||
|
function onOpen(_context)
|
||||||
|
render()
|
||||||
|
end
|
||||||
|
|
||||||
|
function onCloseClicked()
|
||||||
|
panel.close()
|
||||||
|
end
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
noctalia.mkdirAll("~/Config-Swap")
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
function update()
|
||||||
|
barWidget.setGlyph("file")
|
||||||
|
end
|
||||||
|
|
||||||
|
function onClick()
|
||||||
|
noctalia.togglePanel("tadomika_ari/config-swap:panel")
|
||||||
|
end
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
id = "tadomika_ari/config-swap"
|
||||||
|
name = "Config Swap"
|
||||||
|
version = "1.0.0"
|
||||||
|
plugin_api = 4
|
||||||
|
author = "TadomiKa-Ari"
|
||||||
|
license = "MIT"
|
||||||
|
dependencies = ["cp", "rm"]
|
||||||
|
icon = "file"
|
||||||
|
description = "A Noctalia widget for applying Configuration file with one click"
|
||||||
|
tags = ["utility"]
|
||||||
|
|
||||||
|
[[widget]]
|
||||||
|
id = "widget"
|
||||||
|
entry = "config-swap-widget.luau"
|
||||||
|
|
||||||
|
[[panel]]
|
||||||
|
id = "panel"
|
||||||
|
entry = "config-swap-panel.luau"
|
||||||
|
width = 1000
|
||||||
|
height = 800
|
||||||
|
|
||||||
|
[[service]]
|
||||||
|
id = "start"
|
||||||
|
entry = "config-swap-service.luau"
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"panel": {
|
||||||
|
"apply": "Apply",
|
||||||
|
"apply_notification_title": "Apply",
|
||||||
|
"created_by": "Created by {author}",
|
||||||
|
"description": "Description: {description}",
|
||||||
|
"download_failed": "Download failed",
|
||||||
|
"download_ok": "Download complete",
|
||||||
|
"install": "Install",
|
||||||
|
"install_notification_title": "Install",
|
||||||
|
"installed_title": "Installed Configs",
|
||||||
|
"notification_title": "Config Swap",
|
||||||
|
"origin": "Origin: {origin}",
|
||||||
|
"refresh": "Refresh",
|
||||||
|
"show_list": "Show List",
|
||||||
|
"show_store": "Show Store",
|
||||||
|
"store_title": "Config Store",
|
||||||
|
"title": "Config Swap",
|
||||||
|
"welcome_message_title": "Welcome to Config Swap!",
|
||||||
|
"welcome_message1": "Please read this carefully before you continue.",
|
||||||
|
"welcome_message2": "This plugin can replace your current Noctalia configuration. We recommend backing up your current settings first — use the Save button in the Installed section. Backups are stored in ~/Config-Swap.",
|
||||||
|
"not_agree": "Not now",
|
||||||
|
"agree": "I understand",
|
||||||
|
"confirmation": "Apply this configuration?",
|
||||||
|
"description_confirmation": "This will replace your current settings.toml and wallpaper. A backup is not created automatically unless you saved one first.",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirmation_delete": "Are you sure too delete ",
|
||||||
|
"delete": "delete",
|
||||||
|
"description_setting1": "Save your current configuration under a custom name at ~/Config-Swap/{name}. A default info.json is created and can be edited later at ~/Config-Swap/{name}/info.json. To set a preview, you can add preview.png to the config folder. This also works for wallpapers using wallpaper.png",
|
||||||
|
"save": "save",
|
||||||
|
"info_store_title": "Info Store section",
|
||||||
|
"info_store_github_button": "github",
|
||||||
|
"info_store_copy_link_success": "copy link success",
|
||||||
|
"info_store_what_is_title": "What is the Store?",
|
||||||
|
"info_store_what_is_body": "The Store lists community-submitted configuration profiles hosted on the Config-Swap-Box GitHub repository. Each profile bundles a settings.toml, a preview image, and an optional wallpaper.",
|
||||||
|
"info_store_installing_title": "Installing a profile",
|
||||||
|
"info_store_installing_body": "Pressing install downloads the profile's files into ~/Config-Swap/{name}. This does not change your current configuration yet.",
|
||||||
|
"info_store_applying_title": "Applying a profile",
|
||||||
|
"info_store_applying_body": "Pressing apply replaces your active settings.toml and wallpaper with the selected profile. This is not reversible unless you saved a backup first from the Setting section.",
|
||||||
|
"info_store_community_title": "Community content",
|
||||||
|
"info_store_community_body": "Profiles are submitted by community members and are reviewed by the owner.",
|
||||||
|
"info_store_rate_limits_title": "Rate limits",
|
||||||
|
"info_store_rate_limits_body": "The store list is fetched from the GitHub API, which limits unauthenticated requests. Avoid refreshing repeatedly in a short period of time.",
|
||||||
|
"info_how_to_submite_config_title": "How to submit a configuration",
|
||||||
|
"info_how_to_submite_config_body": "You can contribute to Config Swap by adding your own configuration files and wallpaper to the store so others can use them.\n\nTo do so:\n* Go to the Settings section and save your configuration. The plugin will copy your settings.toml and create an info.json file.\n* Take a preview image and wallpaper, then go to ~/Config-Swap/{name} and copy your preview.png and wallpaper.png files. The exact names are required.\n* Update your info.json with important information such as the author and description.\n* Go to the GitHub page linked from the information panel in the Store section and create a pull request.\n* Wait for review, and then your configuration can be shared."
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user