diff --git a/config-swap/README.md b/config-swap/README.md new file mode 100644 index 00000000..6f0dfe79 --- /dev/null +++ b/config-swap/README.md @@ -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. \ No newline at end of file diff --git a/config-swap/config-swap-panel.luau b/config-swap/config-swap-panel.luau new file mode 100644 index 00000000..47071a22 --- /dev/null +++ b/config-swap/config-swap-panel.luau @@ -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 \ No newline at end of file diff --git a/config-swap/config-swap-service.luau b/config-swap/config-swap-service.luau new file mode 100644 index 00000000..ee8a9f76 --- /dev/null +++ b/config-swap/config-swap-service.luau @@ -0,0 +1 @@ +noctalia.mkdirAll("~/Config-Swap") \ No newline at end of file diff --git a/config-swap/config-swap-widget.luau b/config-swap/config-swap-widget.luau new file mode 100644 index 00000000..e9b9d62a --- /dev/null +++ b/config-swap/config-swap-widget.luau @@ -0,0 +1,7 @@ +function update() + barWidget.setGlyph("file") +end + +function onClick() + noctalia.togglePanel("tadomika_ari/config-swap:panel") +end \ No newline at end of file diff --git a/config-swap/plugin.toml b/config-swap/plugin.toml new file mode 100644 index 00000000..489d94ca --- /dev/null +++ b/config-swap/plugin.toml @@ -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" \ No newline at end of file diff --git a/config-swap/thumbnail.webp b/config-swap/thumbnail.webp new file mode 100644 index 00000000..fb975e84 Binary files /dev/null and b/config-swap/thumbnail.webp differ diff --git a/config-swap/translations/en.json b/config-swap/translations/en.json new file mode 100644 index 00000000..5c48dc57 --- /dev/null +++ b/config-swap/translations/en.json @@ -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." + } +} \ No newline at end of file