From 83ac171c34cf35344ca29610d5d272eb93d4ae94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B6=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=90=D1=80?= =?UTF-8?q?=D1=82=D0=B5=D0=BC?= Date: Fri, 13 Feb 2026 20:03:15 +0300 Subject: [PATCH 1/2] Add Integration Wizard --- .github/workflows/pages-wizard.yml | 50 + content/scripts/generate-wizard-version.sh | 55 + content/wizard/app.js | 1239 ++++++++++++++++++++ content/wizard/app/clipboard.js | 16 + content/wizard/app/data.js | 34 + content/wizard/app/i18n.js | 85 ++ content/wizard/app/output.js | 627 ++++++++++ content/wizard/app/router.js | 134 +++ content/wizard/app/state.js | 129 ++ content/wizard/data/modules.json | 194 +++ content/wizard/data/version.json | 3 + content/wizard/favicon.ico | Bin 0 -> 4286 bytes content/wizard/i18n/en.json | 274 +++++ content/wizard/i18n/es.json | 274 +++++ content/wizard/i18n/ja.json | 274 +++++ content/wizard/i18n/pt.json | 274 +++++ content/wizard/i18n/ru.json | 274 +++++ content/wizard/i18n/zh-Hans.json | 274 +++++ content/wizard/index.html | 46 + content/wizard/style.css | 545 +++++++++ 20 files changed, 4801 insertions(+) create mode 100644 .github/workflows/pages-wizard.yml create mode 100755 content/scripts/generate-wizard-version.sh create mode 100644 content/wizard/app.js create mode 100644 content/wizard/app/clipboard.js create mode 100644 content/wizard/app/data.js create mode 100644 content/wizard/app/i18n.js create mode 100644 content/wizard/app/output.js create mode 100644 content/wizard/app/router.js create mode 100644 content/wizard/app/state.js create mode 100644 content/wizard/data/modules.json create mode 100644 content/wizard/data/version.json create mode 100644 content/wizard/favicon.ico create mode 100644 content/wizard/i18n/en.json create mode 100644 content/wizard/i18n/es.json create mode 100644 content/wizard/i18n/ja.json create mode 100644 content/wizard/i18n/pt.json create mode 100644 content/wizard/i18n/ru.json create mode 100644 content/wizard/i18n/zh-Hans.json create mode 100644 content/wizard/index.html create mode 100644 content/wizard/style.css diff --git a/.github/workflows/pages-wizard.yml b/.github/workflows/pages-wizard.yml new file mode 100644 index 00000000..6316a249 --- /dev/null +++ b/.github/workflows/pages-wizard.yml @@ -0,0 +1,50 @@ +name: pages-wizard + +on: + push: + branches: + - main + paths: + - content/wizard/** + - content/scripts/generate-wizard-version.sh + - version.properties + - .github/workflows/publish.yml + - .github/workflows/pages-wizard.yml + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages-wizard + cancel-in-progress: true + +jobs: + deploy: + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Generate wizard version.json + run: ./content/scripts/generate-wizard-version.sh + + - name: Configure Pages + uses: actions/configure-pages@v5 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: content/wizard + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/content/scripts/generate-wizard-version.sh b/content/scripts/generate-wizard-version.sh new file mode 100755 index 00000000..b514feb4 --- /dev/null +++ b/content/scripts/generate-wizard-version.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Mirrors publish.yml version logic: release version comes from tag name `vX.Y.Z`. +# For non-tag runs, fallback to latest release tag, then version.properties. +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +OUTPUT_FILE="$ROOT_DIR/content/wizard/data/version.json" +VERSION_FILE="$ROOT_DIR/version.properties" + +extract_from_tag() { + local tag="$1" + if [[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]]; then + echo "${tag#v}" + return 0 + fi + return 1 +} + +version="" + +if [[ -n "${GITHUB_REF_NAME:-}" ]]; then + if version_from_ref="$(extract_from_tag "$GITHUB_REF_NAME")"; then + version="$version_from_ref" + fi +fi + +if [[ -z "$version" ]]; then + latest_tag="$(git -C "$ROOT_DIR" tag --list 'v*.*.*' --sort=-v:refname | head -n 1 || true)" + if [[ -n "$latest_tag" ]]; then + if version_from_tag="$(extract_from_tag "$latest_tag")"; then + version="$version_from_tag" + fi + fi +fi + +if [[ -z "$version" ]] && [[ -f "$VERSION_FILE" ]]; then + version_from_properties="$(awk -F'=' '/^libraryVersionName=/{print $2}' "$VERSION_FILE" | tr -d '[:space:]')" + if [[ -n "$version_from_properties" ]]; then + version="$version_from_properties" + fi +fi + +if [[ -z "$version" ]]; then + version="1.0.0" +fi + +mkdir -p "$(dirname "$OUTPUT_FILE")" +cat > "$OUTPUT_FILE" </g, ">") + .replace(/\"/g, """) + .replace(/'/g, "'"); +} + +function uniqueArray(values) { + return values.filter((value, index) => values.indexOf(value) === index); +} + +function getPlatformLabel(platformId) { + return t(PLATFORM_TITLE_KEYS[platformId]); +} + +function moduleHasConfigPages(moduleId) { + const module = modulesById[moduleId]; + return Boolean(module && Array.isArray(module.configPages) && module.configPages.length > 0); +} + +function syncModuleFlow() { + state.moduleFlowIds = state.selectedModules.filter((moduleId) => moduleHasConfigPages(moduleId)); +} + +function getDefaultModuleConfig(moduleId) { + switch (moduleId) { + case "logging": + return { + integrateNapier: false, + labelExtractor: "bracket", + }; + case "control_panel": + return { + items: [], + }; + case "multiplatform_settings": + return { + storages: [{ displayName: "Default" }], + }; + case "overlay": + return { + enablePerformanceProvider: true, + }; + case "runner": + return { + generateSampleCalls: false, + }; + default: + return {}; + } +} + +function ensureModuleConfig(moduleId) { + if (!state.moduleConfigs[moduleId] || typeof state.moduleConfigs[moduleId] !== "object") { + state.moduleConfigs[moduleId] = getDefaultModuleConfig(moduleId); + persistState(); + } + return state.moduleConfigs[moduleId]; +} + +function sanitizeState() { + const validModuleIds = new Set(modules.map((module) => module.id)); + state.platforms = uniqueArray(state.platforms.filter((platform) => PLATFORMS.includes(platform))); + state.selectedModules = uniqueArray(state.selectedModules.filter((moduleId) => validModuleIds.has(moduleId))); + + Object.keys(state.moduleConfigs).forEach((moduleId) => { + if (!validModuleIds.has(moduleId)) { + delete state.moduleConfigs[moduleId]; + } + }); + + if (!SUPPORTED_LANGUAGES.includes(state.lang)) { + state.lang = DEFAULT_LANGUAGE; + } +} + +function persistState() { + saveStateToStorage(state); + saveUrlState(state); +} + +function showToast(message) { + clearTimeout(toastTimer); + toastElement.textContent = message; + toastElement.classList.add("visible"); + toastTimer = setTimeout(() => { + toastElement.classList.remove("visible"); + }, 1600); +} + +function getUnsupportedPlatformIds(moduleDescription) { + return getUnsupportedPlatforms(moduleDescription, state.platforms); +} + +function getUnsupportedPlatformLabels(moduleDescription) { + return getUnsupportedPlatformIds(moduleDescription).map((platform) => getPlatformLabel(platform)); +} + +function buildModuleWarning(moduleDescription) { + const unsupportedLabels = getUnsupportedPlatformLabels(moduleDescription); + if (unsupportedLabels.length === 0) { + return ""; + } + + return ` +
+
${escapeHtml(t("modules.unsupportedWarning", { platforms: unsupportedLabels.join(", ") }))}
+
+ `; +} + +function renderWelcomePage() { + const selectedPlatforms = state.platforms.length; + const selectedModules = state.selectedModules.length; + + return ` +
+

${escapeHtml(t("step.welcome.title"))}

+

${escapeHtml(t("step.welcome.subtitle"))}

+ +
+

${escapeHtml(t("step.welcome.checklistTitle"))}

+

${escapeHtml(t("step.welcome.checklistSubtitle"))}

+
    +
  • ${escapeHtml(t("step.welcome.itemLanguage"))}
  • +
  • ${escapeHtml(t("step.welcome.itemPlatforms"))}
  • +
  • ${escapeHtml(t("step.welcome.itemModules"))}
  • +
  • ${escapeHtml(t("step.welcome.itemOutput"))}
  • +
+
+ +
+
+

${escapeHtml(t("summary.platforms"))}

+

${escapeHtml(t("summary.count", { count: selectedPlatforms }))}

+
+
+

${escapeHtml(t("summary.modules"))}

+

${escapeHtml(t("summary.count", { count: selectedModules }))}

+
+
+

${escapeHtml(t("summary.version"))}

+

${escapeHtml(kickVersion)}

+
+
+
+ `; +} + +function renderPlatformsPage() { + const cards = PLATFORMS.map((platform) => { + const selected = state.platforms.includes(platform); + return ` + + `; + }).join("\n"); + + const warning = state.platforms.length === 0 + ? `

${escapeHtml(t("validation.platformRequired"))}

` + : ""; + + return ` +
+

${escapeHtml(t("step.platforms.title"))}

+

${escapeHtml(t("step.platforms.subtitle"))}

+ ${warning} +
${cards}
+
+ `; +} + +function renderModulesPage() { + const cards = modules.map((moduleDescription) => { + const selected = state.selectedModules.includes(moduleDescription.id); + const warnings = buildModuleWarning(moduleDescription); + + const highBadge = moduleDescription.configComplexity === "high" && moduleDescription.id !== "control_panel" + ? `${escapeHtml(t("badge.highConfiguration"))}` + : ""; + + return ` + + `; + }).join("\n"); + + const warning = state.selectedModules.length === 0 + ? `

${escapeHtml(t("validation.moduleRequired"))}

` + : ""; + + return ` +
+

${escapeHtml(t("step.modules.title"))}

+

${escapeHtml(t("step.modules.subtitle"))}

+

${escapeHtml(t("step.modules.selectedCount", { count: state.selectedModules.length }))}

+ ${warning} +
${cards}
+
+ `; +} + +function renderLoggingConfig(config) { + const integrateNapier = config.integrateNapier === true; + const labelExtractor = config.labelExtractor === "custom" ? "custom" : "bracket"; + + return ` +
+ + +
+ ${escapeHtml(t("config.advanced"))} +
+ + +
+
+
+ `; +} + +function renderSettingsConfig(config) { + const storages = Array.isArray(config.storages) && config.storages.length > 0 + ? config.storages + : [{ displayName: "Default" }]; + + const rows = storages.map((storage, index) => ` +
+
+
${escapeHtml(t("config.storageItem", { index: index + 1 }))}
+ +
+
+ + +
+
+ `).join("\n"); + + return ` +
+ +
${rows}
+
+ `; +} + +function renderOverlayConfig(config) { + const enablePerformanceProvider = config.enablePerformanceProvider !== false; + + return ` +
+ +
+ `; +} + +function renderRunnerConfig(config) { + const generateSampleCalls = config.generateSampleCalls === true; + + return ` +
+ +
+ `; +} + +function renderControlPanelConfig(config) { + const items = Array.isArray(config.items) ? config.items : []; + + const rows = items.map((item, index) => { + const typeValue = CONTROL_PANEL_ITEM_TYPES.includes(item.type) ? item.type : "string"; + const editorValue = CONTROL_PANEL_EDITORS.includes(item.editor) ? item.editor : "none"; + const listVisible = editorValue === "list" ? "" : "hidden"; + + const typeOptions = CONTROL_PANEL_ITEM_TYPES.map((option) => ` + + `).join(""); + + const editorOptions = CONTROL_PANEL_EDITORS.map((option) => ` + + `).join(""); + + return ` +
+
+
${escapeHtml(t("config.controlPanel.item", { index: index + 1 }))}
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ ${escapeHtml(t("config.advanced"))} +
+ + +
+ +
+ + +
+
+
+ `; + }).join("\n"); + + return ` +
+
+ + +
+
${rows}
+
+ `; +} + +function renderGenericConfig() { + return ` +
+

${escapeHtml(t("config.noQuestions"))}

+
+ `; +} + +function renderModuleConfigPage(moduleId) { + const moduleDescription = modulesById[moduleId]; + if (!moduleDescription || !moduleHasConfigPages(moduleId)) { + return renderModulesPage(); + } + + const config = ensureModuleConfig(moduleId); + const highBadge = moduleDescription.configComplexity === "high" && moduleDescription.id !== "control_panel" + ? `${escapeHtml(t("badge.highConfiguration"))}` + : ""; + + let form = renderGenericConfig(); + + switch (moduleId) { + case "logging": + form = renderLoggingConfig(config); + break; + case "control_panel": + form = renderControlPanelConfig(config); + break; + case "multiplatform_settings": + form = renderSettingsConfig(config); + break; + case "overlay": + form = renderOverlayConfig(config); + break; + case "runner": + form = renderRunnerConfig(config); + break; + default: + form = renderGenericConfig(); + break; + } + + return ` +
+

${escapeHtml(t("step.moduleConfig.title", { module: t(moduleDescription.titleKey) }))}

+

${escapeHtml(t(moduleDescription.descriptionKey))}

+
${highBadge}
+ ${buildModuleWarning(moduleDescription)} + ${form} +
+ `; +} + +function getGlueGuideText(item) { + if (item.type === "logging") { + return item.integrateNapier === true + ? t("glue.guide.loggingWithNapier") + : t("glue.guide.logging"); + } + if (item.type === "ktor3") { + return t("glue.guide.ktor3"); + } + if (item.type === "firebase_cloud_messaging") { + if (item.includeAndroid && item.includeIos) { + return t("glue.guide.firebaseCloudMessagingBoth"); + } + if (item.includeAndroid) { + return t("glue.guide.firebaseCloudMessagingAndroid"); + } + if (item.includeIos) { + return t("glue.guide.firebaseCloudMessagingIos"); + } + return t("glue.guide.firebaseCloudMessagingGeneric"); + } + if (item.type === "firebase_analytics") { + return t("glue.guide.firebaseAnalytics"); + } + if (item.type === "control_panel") { + return t("glue.guide.controlPanel"); + } + if (item.type === "overlay") { + return t("glue.guide.overlay"); + } + if (item.type === "runner") { + return t("glue.guide.runner"); + } + return ""; +} + +function buildKtor3ExampleSnippet() { + return [ + "val httpClient = HttpClient {", + " install(KickKtor3Plugin) {", + " maxBodySizeBytes = 1024 * 1024L", + " }", + "}", + ].join("\n"); +} + +function buildFirebaseCloudMessagingExampleSnippet(item) { + const parts = []; + + if (item.includeAndroid) { + parts.push( + [ + "// Android", + "class MyMessagingService : FirebaseMessagingService() {", + " override fun onMessageReceived(message: RemoteMessage) {", + " // your app logic...", + " Kick.firebaseCloudMessaging.handleFcm(message)", + " }", + "}", + ].join("\n") + ); + } + + if (item.includeIos) { + parts.push( + [ + "// Shared Kotlin bridge for iOS push callbacks", + "object IosPushBridge {", + " fun onApnsPayload(userInfo: Map) {", + " Kick.firebaseCloudMessaging.handleApnsPayload(userInfo)", + " }", + "}", + ].join("\n") + ); + } + + if (parts.length === 0) { + return [ + "object PushBridge {", + " fun onPushPayload(payload: Map) {", + " Kick.firebaseCloudMessaging.handleApnsPayload(payload)", + " }", + "}", + ].join("\n"); + } + + return parts.join("\n\n"); +} + +function buildFirebaseAnalyticsExampleSnippet() { + return [ + "class AnalyticsReporter(", + " private val firebaseAnalytics: FirebaseAnalytics,", + ") {", + " fun logEvent(name: String, params: Bundle?) {", + " firebaseAnalytics.logEvent(name, params)", + " Kick.firebaseAnalytics.logEvent(name, params)", + " }", + "", + " fun setUserId(id: String?) {", + " firebaseAnalytics.setUserId(id)", + " Kick.firebaseAnalytics.setUserId(id)", + " }", + "", + " fun setUserProperty(name: String, value: String) {", + " firebaseAnalytics.setUserProperty(name, value)", + " Kick.firebaseAnalytics.setUserProperty(name, value)", + " }", + "}", + ].join("\n"); +} + +function buildControlPanelExampleSnippet() { + return [ + "if (Kick.controlPanel.getBoolean(\"enableSomeRequest\")) {", + " makeRequest(Kick.controlPanel.getString(\"someRequestUrl\"))", + "}", + ].join("\n"); +} + +function buildOverlayExampleSnippet() { + return [ + "Kick.overlay.set(\"fps\", 58)", + "Kick.overlay.set(\"isWsConnected\", true)", + "Kick.overlay.set(\"requestsInFlight\", 3, \"Network\")", + "Kick.overlay.set(\"screen\", \"Home\", \"UI\")", + ].join("\n"); +} + +function buildRunnerExampleSnippet() { + return [ + "Kick.runner.addCall(", + " title = \"JSON sample\",", + " description = \"Pretty-printed JSON output\",", + " renderer = JsonRunnerRenderer(),", + ") {", + " \"\"\"{\\\"message\\\":\\\"Hello, Runner!\\\",\\\"timestamp\\\":${DateUtils.currentTimeMillis()}}\"\"\"", + "}", + ].join("\n"); +} + +function buildGuideExample(item) { + if (item.type === "ktor3") { + return { + titleKey: "output.steps.ktorExampleTitle", + descriptionKey: "output.steps.ktorExampleDescription", + code: buildKtor3ExampleSnippet(), + }; + } + + if (item.type === "firebase_cloud_messaging") { + return { + titleKey: "output.steps.fcmExampleTitle", + descriptionKey: "output.steps.fcmExampleDescription", + code: buildFirebaseCloudMessagingExampleSnippet(item), + }; + } + + if (item.type === "firebase_analytics") { + return { + titleKey: "output.steps.analyticsExampleTitle", + descriptionKey: "output.steps.analyticsExampleDescription", + code: buildFirebaseAnalyticsExampleSnippet(), + }; + } + + if (item.type === "control_panel") { + return { + titleKey: "output.steps.controlPanelExampleTitle", + descriptionKey: "output.steps.controlPanelExampleDescription", + code: buildControlPanelExampleSnippet(), + }; + } + + if (item.type === "overlay") { + return { + titleKey: "output.steps.overlayExampleTitle", + descriptionKey: "output.steps.overlayExampleDescription", + code: buildOverlayExampleSnippet(), + }; + } + + if (item.type === "runner") { + return { + titleKey: "output.steps.runnerExampleTitle", + descriptionKey: "output.steps.runnerExampleDescription", + code: buildRunnerExampleSnippet(), + }; + } + + return null; +} + +function renderOutputCodeBlock({ title, description, copyKey, code }) { + const descriptionLine = description + ? `

${escapeHtml(description)}

` + : ""; + + return ` +
+
+
+

${escapeHtml(title)}

+ ${descriptionLine} +
+ +
+
${escapeHtml(code)}
+
+ `; +} + +function renderOutputPage() { + outputCache = buildOutput(state, modulesById, kickVersion); + const copyMap = { + gradle: outputCache.gradle, + common: outputCache.commonKotlin, + }; + const steps = []; + + steps.push(` +
  • +

    ${escapeHtml(t("output.steps.gradle"))}

    + ${renderOutputCodeBlock({ + title: t("output.gradle.title"), + description: t("output.gradle.description"), + copyKey: "gradle", + code: outputCache.gradle, + })} +
  • + `); + + const commonPath = "shared/src/commonMain/kotlin/KickBootstrap.kt"; + steps.push(` +
  • +

    ${escapeHtml(t("output.steps.file", { path: commonPath }))}

    + ${renderOutputCodeBlock({ + title: t("output.common.title"), + description: t("output.common.description"), + copyKey: "common", + code: outputCache.commonKotlin, + })} +
  • + `); + + outputCache.glue.files.forEach((file, index) => { + const key = `glue-file-${index}`; + copyMap[key] = file.code; + steps.push(` +
  • +

    ${escapeHtml(t("output.steps.file", { path: file.path }))}

    + ${renderOutputCodeBlock({ + title: file.title, + description: `${t("output.platformGlue.pathLabel")} ${file.path}`, + copyKey: key, + code: file.code, + })} +
  • + `); + }); + + (outputCache.glue.guideItems || []).forEach((item, index) => { + const text = getGlueGuideText(item); + if (!text) { + return; + } + + let extraCodeBlock = ""; + const example = buildGuideExample(item); + if (example) { + const key = `guide-${item.type}-${index}`; + const snippet = example.code; + copyMap[key] = snippet; + extraCodeBlock = renderOutputCodeBlock({ + title: t(example.titleKey), + description: t(example.descriptionKey), + copyKey: key, + code: snippet, + }); + } + + steps.push(` +
  • +

    ${escapeHtml(text)}

    + ${extraCodeBlock} +
  • + `); + }); + + outputCache.copyMap = copyMap; + + return ` +
    +

    ${escapeHtml(t("step.output.title"))}

    +

    ${escapeHtml(t("step.output.subtitle"))}

    +
      + ${steps.join("\n")} +
    +
    + `; +} + +function renderPage(routeInfo) { + if (routeInfo.route === "/") { + return renderWelcomePage(); + } + if (routeInfo.route === "/platforms") { + return renderPlatformsPage(); + } + if (routeInfo.route === "/modules") { + return renderModulesPage(); + } + if (routeInfo.route === "/output") { + return renderOutputPage(); + } + if (routeInfo.type === "module") { + return renderModuleConfigPage(routeInfo.moduleId); + } + return renderWelcomePage(); +} + +function updateFooter(routeInfo) { + const safeRoute = ensureRouteAllowed(routeInfo, state); + const { index, total } = getStepPosition(safeRoute, state); + const adjacent = getAdjacentRoutes(safeRoute, state); + const canSkipModule = routeInfo.type === "module" && safeRoute === routeInfo.route; + + progressElement.textContent = t("progress.step", { + current: index + 1, + total, + }); + + skipModuleButton.textContent = t("actions.skipModule"); + skipModuleButton.classList.toggle("hidden", !canSkipModule); + skipModuleButton.disabled = !canSkipModule; + + backButton.disabled = !adjacent.prev; + nextButton.disabled = safeRoute !== "/output" && !adjacent.next; + + if (!adjacent.next) { + nextButton.textContent = t("nav.done"); + } else if (safeRoute === "/modules") { + nextButton.textContent = t("nav.nextModule"); + } else { + nextButton.textContent = t("nav.next"); + } +} + +function validateBeforeNext(routeInfo) { + if (routeInfo.route === "/platforms" && state.platforms.length === 0) { + showToast(t("validation.platformRequired")); + return false; + } + + if (routeInfo.route === "/modules" && state.selectedModules.length === 0) { + showToast(t("validation.moduleRequired")); + return false; + } + + return true; +} + +function renderLanguageOptions() { + languageSelect.innerHTML = SUPPORTED_LANGUAGES + .map((lang) => ``) + .join(""); + languageSelect.value = state.lang; +} + +function render() { + syncModuleFlow(); + const parsedRoute = parseCurrentRoute(); + const safeRoute = ensureRouteAllowed(parsedRoute, state); + + if (safeRoute !== parsedRoute.route) { + navigateTo(safeRoute, { replace: true }); + return; + } + + document.documentElement.lang = state.lang; + document.title = t("app.pageTitle"); + + renderLanguageOptions(); + i18n.applyToDocument(document.body); + + appElement.innerHTML = renderPage(parsedRoute); + updateFooter(parsedRoute); + persistState(); +} + +function togglePlatform(platformId) { + if (!PLATFORMS.includes(platformId)) { + return; + } + if (state.platforms.includes(platformId)) { + state.platforms = state.platforms.filter((entry) => entry !== platformId); + } else { + state.platforms = [...state.platforms, platformId]; + } + render(); +} + +function toggleModule(moduleId) { + if (!modulesById[moduleId]) { + return; + } + const selected = state.selectedModules.includes(moduleId); + if (selected) { + state.selectedModules = state.selectedModules.filter((entry) => entry !== moduleId); + } else { + state.selectedModules = [...state.selectedModules, moduleId]; + if (moduleHasConfigPages(moduleId)) { + ensureModuleConfig(moduleId); + } + } + render(); +} + +function skipCurrentModule() { + syncModuleFlow(); + const routeInfo = parseCurrentRoute(); + const safeRoute = ensureRouteAllowed(routeInfo, state); + if (routeInfo.type !== "module" || safeRoute !== routeInfo.route || !routeInfo.moduleId) { + return; + } + + const adjacent = getAdjacentRoutes(safeRoute, state); + const nextRoute = adjacent.next || "/output"; + state.selectedModules = state.selectedModules.filter((entry) => entry !== routeInfo.moduleId); + syncModuleFlow(); + persistState(); + navigateTo(nextRoute); +} + +function mutateModuleConfig(moduleId, mutator, rerender = false) { + const config = ensureModuleConfig(moduleId); + mutator(config); + persistState(); + if (rerender) { + render(); + } +} + +function addStorage() { + mutateModuleConfig("multiplatform_settings", (config) => { + const storages = Array.isArray(config.storages) ? config.storages : []; + storages.push({ displayName: `${t("config.storageDefaultName")} ${storages.length + 1}` }); + config.storages = storages; + }, true); +} + +function removeStorage(index) { + mutateModuleConfig("multiplatform_settings", (config) => { + const storages = Array.isArray(config.storages) ? config.storages : []; + storages.splice(index, 1); + if (storages.length === 0) { + storages.push({ displayName: "Default" }); + } + config.storages = storages; + }, true); +} + +function setStorageName(index, value) { + mutateModuleConfig("multiplatform_settings", (config) => { + const storages = Array.isArray(config.storages) ? config.storages : []; + if (!storages[index]) { + storages[index] = { displayName: "" }; + } + storages[index].displayName = value; + config.storages = storages; + }); +} + +function addControlItem() { + mutateModuleConfig("control_panel", (config) => { + const items = Array.isArray(config.items) ? config.items : []; + items.push({ + name: "", + type: "string", + category: "", + editor: "none", + listValues: "", + }); + config.items = items; + }, true); +} + +function addControlExamples() { + mutateModuleConfig("control_panel", (config) => { + config.items = [ + { + name: "featureEnabled", + type: "bool", + category: "General", + editor: "none", + listValues: "", + }, + { + name: "maxItems", + type: "int", + category: "General", + editor: "input_number", + listValues: "", + }, + { + name: "endpoint", + type: "string", + category: "Network", + editor: "input_string", + listValues: "", + }, + { + name: "environment", + type: "list", + category: "Network", + editor: "list", + listValues: "dev, stage, prod", + }, + { + name: "refresh_cache", + type: "button", + category: "Actions", + editor: "none", + listValues: "", + }, + ]; + }, true); +} + +function removeControlItem(index) { + mutateModuleConfig("control_panel", (config) => { + const items = Array.isArray(config.items) ? config.items : []; + items.splice(index, 1); + config.items = items; + }, true); +} + +function setControlItemField(index, key, value, rerender = false) { + mutateModuleConfig("control_panel", (config) => { + const items = Array.isArray(config.items) ? config.items : []; + if (!items[index]) { + items[index] = { + name: "", + type: "string", + category: "", + editor: "none", + listValues: "", + }; + } + items[index][key] = value; + config.items = items; + }, rerender); +} + +async function onLanguageChange(event) { + const lang = event.target.value; + if (!SUPPORTED_LANGUAGES.includes(lang)) { + return; + } + state.lang = lang; + await i18n.loadLanguage(lang); + render(); +} + +function onAppClick(event) { + const actionNode = event.target.closest("[data-action]"); + if (!actionNode) { + return; + } + + const action = actionNode.dataset.action; + + if (action === "toggle-platform") { + togglePlatform(actionNode.dataset.platformId); + } + + if (action === "toggle-module") { + toggleModule(actionNode.dataset.moduleId); + } + + if (action === "add-storage") { + addStorage(); + } + + if (action === "remove-storage") { + removeStorage(Number(actionNode.dataset.index)); + } + + if (action === "add-control-item") { + addControlItem(); + } + + if (action === "add-control-examples") { + addControlExamples(); + } + + if (action === "remove-control-item") { + removeControlItem(Number(actionNode.dataset.index)); + } + + if (action === "copy-block") { + const key = actionNode.dataset.copyKey; + const map = outputCache?.copyMap || {}; + const value = map[key] || ""; + if (value) { + copyText(value) + .then(() => showToast(t("toast.copied"))) + .catch(() => showToast(t("toast.copyFailed"))); + } + } +} + +function onAppInput(event) { + const action = event.target.dataset.action; + + if (action === "set-storage-name") { + setStorageName(Number(event.target.dataset.index), event.target.value); + } + + if (action === "set-control-name") { + setControlItemField(Number(event.target.dataset.index), "name", event.target.value); + } + + if (action === "set-control-category") { + setControlItemField(Number(event.target.dataset.index), "category", event.target.value); + } + + if (action === "set-control-list-values") { + setControlItemField(Number(event.target.dataset.index), "listValues", event.target.value); + } +} + +function onAppChange(event) { + const action = event.target.dataset.action; + + if (action === "set-logging-napier") { + mutateModuleConfig("logging", (config) => { + config.integrateNapier = event.target.checked; + }); + } + + if (action === "set-logging-extractor") { + mutateModuleConfig("logging", (config) => { + config.labelExtractor = event.target.value; + }); + } + + if (action === "set-overlay-performance") { + mutateModuleConfig("overlay", (config) => { + config.enablePerformanceProvider = event.target.checked; + }); + } + + if (action === "set-runner-samples") { + mutateModuleConfig("runner", (config) => { + config.generateSampleCalls = event.target.checked; + }); + } + + if (action === "set-control-type") { + setControlItemField(Number(event.target.dataset.index), "type", event.target.value, true); + } + + if (action === "set-control-editor") { + setControlItemField(Number(event.target.dataset.index), "editor", event.target.value, true); + } +} + +function goBack() { + syncModuleFlow(); + const routeInfo = parseCurrentRoute(); + const safeRoute = ensureRouteAllowed(routeInfo, state); + const adjacent = getAdjacentRoutes(safeRoute, state); + if (adjacent.prev) { + navigateTo(adjacent.prev); + } +} + +function goNext() { + syncModuleFlow(); + const routeInfo = parseCurrentRoute(); + const safeRoute = ensureRouteAllowed(routeInfo, state); + if (!validateBeforeNext({ ...routeInfo, route: safeRoute })) { + return; + } + const adjacent = getAdjacentRoutes(safeRoute, state); + if (adjacent.next) { + navigateTo(adjacent.next); + return; + } + if (safeRoute === "/output") { + navigateTo("/"); + } +} + +function resetWizard() { + const allowReset = window.confirm(t("nav.resetConfirm")); + if (!allowReset) { + return; + } + + const lang = state.lang; + state = createDefaultState(); + state.lang = lang; + clearStateStorage(); + clearUrlState(); + persistState(); + navigateTo("/", { replace: true }); + render(); +} + +async function initialize() { + try { + const [loadedModules, loadedVersion] = await Promise.all([loadModules(), loadVersion()]); + modules = loadedModules; + modulesById = mapModulesById(modules); + kickVersion = loadedVersion; + + const storageState = loadStateFromStorage(); + const urlState = loadStateFromUrl(); + const urlLanguage = getLanguageFromUrl(); + + state = hydrateState({ + ...createDefaultState(), + ...(storageState || {}), + ...(urlState || {}), + }); + + if (urlLanguage) { + state.lang = urlLanguage; + } + + sanitizeState(); + + await i18n.loadLanguage(DEFAULT_LANGUAGE); + if (state.lang !== DEFAULT_LANGUAGE) { + await i18n.loadLanguage(state.lang); + } + + if (!window.location.hash) { + navigateTo("/", { replace: true }); + } + + render(); + } catch (error) { + console.error(error); + appElement.innerHTML = `

    ${escapeHtml(t("errors.loadFailed"))}

    `; + } +} + +window.addEventListener("hashchange", render); +backButton.addEventListener("click", goBack); +nextButton.addEventListener("click", goNext); +skipModuleButton.addEventListener("click", skipCurrentModule); +resetButton.addEventListener("click", resetWizard); +languageSelect.addEventListener("change", onLanguageChange); +appElement.addEventListener("click", onAppClick); +appElement.addEventListener("input", onAppInput); +appElement.addEventListener("change", onAppChange); + +initialize(); diff --git a/content/wizard/app/clipboard.js b/content/wizard/app/clipboard.js new file mode 100644 index 00000000..b65c2eb1 --- /dev/null +++ b/content/wizard/app/clipboard.js @@ -0,0 +1,16 @@ +export async function copyText(value) { + if (navigator.clipboard && navigator.clipboard.writeText) { + await navigator.clipboard.writeText(value); + return; + } + + const textarea = document.createElement("textarea"); + textarea.value = value; + textarea.setAttribute("readonly", "readonly"); + textarea.style.position = "absolute"; + textarea.style.left = "-9999px"; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand("copy"); + document.body.removeChild(textarea); +} diff --git a/content/wizard/app/data.js b/content/wizard/app/data.js new file mode 100644 index 00000000..d56e5bd5 --- /dev/null +++ b/content/wizard/app/data.js @@ -0,0 +1,34 @@ +export async function loadModules() { + const response = await fetch("./data/modules.json"); + if (!response.ok) { + throw new Error("Failed to load modules.json"); + } + const modules = await response.json(); + if (!Array.isArray(modules)) { + throw new Error("modules.json must contain an array"); + } + return modules; +} + +export async function loadVersion() { + try { + const response = await fetch("./data/version.json", { cache: "no-store" }); + if (!response.ok) { + return "1.0.0"; + } + const payload = await response.json(); + if (payload && typeof payload.kickVersion === "string" && payload.kickVersion.length > 0) { + return payload.kickVersion; + } + } catch (_error) { + // Ignore fetch errors in offline fallback mode. + } + return "1.0.0"; +} + +export function mapModulesById(modules) { + return modules.reduce((acc, item) => { + acc[item.id] = item; + return acc; + }, {}); +} diff --git a/content/wizard/app/i18n.js b/content/wizard/app/i18n.js new file mode 100644 index 00000000..83f2c755 --- /dev/null +++ b/content/wizard/app/i18n.js @@ -0,0 +1,85 @@ +const FALLBACK_LANGUAGE = "en"; + +function deepGet(source, key) { + return key.split(".").reduce((acc, segment) => { + if (acc && Object.prototype.hasOwnProperty.call(acc, segment)) { + return acc[segment]; + } + return undefined; + }, source); +} + +function applyParams(template, params = {}) { + return template.replace(/\{(\w+)\}/g, (_, key) => { + if (Object.prototype.hasOwnProperty.call(params, key)) { + return String(params[key]); + } + return `{${key}}`; + }); +} + +export class I18n { + constructor(options) { + this.languages = options.languages; + this.translations = {}; + this.currentLanguage = FALLBACK_LANGUAGE; + } + + async loadLanguage(language) { + const safeLanguage = this.languages.includes(language) ? language : FALLBACK_LANGUAGE; + if (!this.translations[safeLanguage]) { + const response = await fetch(`./i18n/${safeLanguage}.json`); + if (!response.ok) { + throw new Error(`Failed to load locale: ${safeLanguage}`); + } + this.translations[safeLanguage] = await response.json(); + } + this.currentLanguage = safeLanguage; + } + + t(key, params = {}) { + const current = this.translations[this.currentLanguage] || {}; + const fallback = this.translations[FALLBACK_LANGUAGE] || {}; + const value = deepGet(current, key) ?? deepGet(fallback, key) ?? key; + if (typeof value !== "string") { + return key; + } + return applyParams(value, params); + } + + applyToDocument(root = document) { + root.querySelectorAll("[data-i18n]").forEach((element) => { + const key = element.getAttribute("data-i18n"); + if (!key) { + return; + } + element.textContent = this.t(key); + }); + + root.querySelectorAll("[data-i18n-title]").forEach((element) => { + const key = element.getAttribute("data-i18n-title"); + if (!key) { + return; + } + element.title = this.t(key); + }); + + root.querySelectorAll("[data-i18n-placeholder]").forEach((element) => { + const key = element.getAttribute("data-i18n-placeholder"); + if (!key) { + return; + } + element.placeholder = this.t(key); + }); + + root.querySelectorAll("[data-i18n-aria-label]").forEach((element) => { + const key = element.getAttribute("data-i18n-aria-label"); + if (!key) { + return; + } + element.setAttribute("aria-label", this.t(key)); + }); + } +} + +export const DEFAULT_LANGUAGE = FALLBACK_LANGUAGE; diff --git a/content/wizard/app/output.js b/content/wizard/app/output.js new file mode 100644 index 00000000..fdc9c4f6 --- /dev/null +++ b/content/wizard/app/output.js @@ -0,0 +1,627 @@ +const PLATFORM_ORDER = ["android", "ios", "jvm", "wasm"]; + +const PLATFORM_FILE_PATHS = { + android: "shared/src/androidMain/kotlin/KickBootstrap.kt", + ios: "shared/src/iosMain/kotlin/KickBootstrap.kt", + jvm: "shared/src/jvmMain/kotlin/KickBootstrap.kt", + wasm: "shared/src/wasmJsMain/kotlin/KickBootstrap.kt", +}; + +function unique(values) { + const seen = new Set(); + const ordered = []; + values.forEach((value) => { + if (!seen.has(value)) { + seen.add(value); + ordered.push(value); + } + }); + return ordered; +} + +function escapeKotlinString(value) { + return String(value) + .replace(/\\/g, "\\\\") + .replace(/"/g, "\\\"") + .replace(/\n/g, "\\n"); +} + +function normalizeTextList(value) { + if (Array.isArray(value)) { + return value.map((item) => String(item).trim()).filter(Boolean); + } + if (typeof value === "string") { + return value + .split(/[\n,]/) + .map((item) => item.trim()) + .filter(Boolean); + } + return []; +} + +function normalizeSettingsConfig(config) { + const fromArray = Array.isArray(config?.storages) + ? config.storages + .map((item) => (typeof item?.displayName === "string" ? item.displayName.trim() : "")) + .filter(Boolean) + : []; + + if (fromArray.length > 0) { + return fromArray; + } + return ["Default"]; +} + +function normalizeControlPanelConfig(config) { + const rows = Array.isArray(config?.items) ? config.items : []; + return rows + .map((item, index) => { + const name = typeof item?.name === "string" && item.name.trim() + ? item.name.trim() + : `item_${index + 1}`; + const type = typeof item?.type === "string" ? item.type : "string"; + const category = typeof item?.category === "string" ? item.category.trim() : ""; + const editor = typeof item?.editor === "string" ? item.editor : "none"; + const listValues = normalizeTextList(item?.listValues); + return { + name, + type, + category, + editor, + listValues, + }; + }); +} + +function buildControlPanelTypeLine(item) { + switch (item.type) { + case "bool": + return "InputType.Boolean(true)"; + case "int": + return "InputType.Int(0)"; + case "list": + return "InputType.String(\"Option 1\")"; + case "button": { + const safeId = escapeKotlinString(item.name.toLowerCase().replace(/\s+/g, "_")); + return `ActionType.Button(\"${safeId}\")`; + } + case "string": + default: + return "InputType.String(\"Value\")"; + } +} + +function buildControlPanelEditorLine(item) { + if (item.editor === "none") { + return null; + } + + if (item.editor === "input_number") { + return "Editor.InputNumber()"; + } + + if (item.editor === "input_string") { + return "Editor.InputString(singleLine = true)"; + } + + if (item.editor === "list") { + const options = item.listValues.length > 0 ? item.listValues : ["Option 1", "Option 2"]; + const optionsCode = options + .map((value) => `InputType.String(\"${escapeKotlinString(value)}\")`) + .join(", "); + return `Editor.List(listOf(${optionsCode}))`; + } + + return null; +} + +function buildControlPanelItemsFunction(config) { + const rows = normalizeControlPanelConfig(config); + const lines = []; + lines.push(" private fun buildControlPanelItems(): List = listOf("); + + if (rows.length === 0) { + lines.push(" // Add items in wizard config or keep list empty."); + } + + rows.forEach((item) => { + const typeLine = buildControlPanelTypeLine(item); + const editorLine = buildControlPanelEditorLine(item); + lines.push(" ControlPanelItem("); + lines.push(` name = \"${escapeKotlinString(item.name)}\", + type = ${typeLine},`); + if (editorLine) { + lines.push(` editor = ${editorLine},`); + } + if (item.category) { + lines.push(` category = \"${escapeKotlinString(item.category)}\",`); + } + lines.push(" ),"); + }); + + lines.push(" )"); + return lines; +} + +function getSelectedModules(state, modulesById) { + return state.selectedModules + .map((id) => modulesById[id]) + .filter(Boolean); +} + +function requiresPlatformBridgeModule(module, selectedPlatforms) { + const activePlatforms = (selectedPlatforms || []).filter((platform) => PLATFORM_ORDER.includes(platform)); + if (activePlatforms.length === 0) { + return false; + } + return activePlatforms.some((platform) => !module.supportedPlatforms.includes(platform)); +} + +function isPlatformSupported(module, platform) { + return module.supportedPlatforms.includes(platform); +} + +export function getUnsupportedPlatforms(moduleDescription, selectedPlatforms) { + return selectedPlatforms.filter((platform) => !moduleDescription.supportedPlatforms.includes(platform)); +} + +function collectKickModuleEnums(selectedModules) { + const enums = []; + selectedModules.forEach((module) => { + if (module.kickModuleEnum) { + enums.push(module.kickModuleEnum); + } + if (Array.isArray(module.extraKickModuleEnums)) { + module.extraKickModuleEnums.forEach((entry) => { + if (entry) { + enums.push(entry); + } + }); + } + }); + return unique(enums); +} + +function buildGradleSnippet(selectedModules, kickVersion) { + const enums = collectKickModuleEnums(selectedModules); + + const lines = []; + lines.push("import ru.bartwell.kick.gradle.KickEnabled"); + lines.push("import ru.bartwell.kick.gradle.KickModule"); + lines.push(""); + lines.push("plugins {"); + lines.push(` id(\"ru.bartwell.kick\") version \"${kickVersion}\"`); + lines.push("}"); + lines.push(""); + lines.push("kick {"); + lines.push(" enabled = KickEnabled.Auto"); + lines.push(" modules("); + if (enums.length > 0) { + enums.forEach((entry) => { + lines.push(` ${entry},`); + }); + } else { + lines.push(" // Select at least one module supported by KickModule enum."); + } + lines.push(" )"); + lines.push("}"); + lines.push(""); + lines.push("// Enable/disable strategy:"); + lines.push("// Use your own build logic and call enableKick(false) for release variants if needed."); + lines.push("// In CI, force behavior per job with -Pkick.enabled=true or -Pkick.enabled=false."); + + return lines.join("\n"); +} + +function buildCommonSnippet(state, selectedModules, hasPlatformBridge) { + const imports = new Set([ + "ru.bartwell.kick.Kick", + "ru.bartwell.kick.core.data.PlatformContext", + "ru.bartwell.kick.runtime.init", + ]); + + const depLines = []; + const preInitLines = []; + const moduleLines = []; + const postInitLines = []; + const helperFunctions = []; + + let hasNapierBridge = false; + let hasRunnerSamples = false; + const hasSqlDelightBridge = selectedModules.some((module) => module.id === "sqldelight"); + const hasRoomBridge = selectedModules.some((module) => module.id === "room"); + + if (hasSqlDelightBridge) { + imports.add("ru.bartwell.kick.module.sqlite.adapter.sqldelight.SqlDelightWrapper"); + depLines.push(" // Build SqlDelightWrapper in your app code and pass it via KickDeps."); + depLines.push(" val sqlDelightWrapper: SqlDelightWrapper? = null,"); + } + + if (hasRoomBridge) { + imports.add("ru.bartwell.kick.module.sqlite.adapter.room.RoomWrapper"); + depLines.push(" // Build RoomWrapper in your app code and pass it via KickDeps."); + depLines.push(" // Keep roomWrapper = null on platforms where Room is not supported."); + depLines.push(" val roomWrapper: RoomWrapper? = null,"); + } + + selectedModules.forEach((module) => { + if (requiresPlatformBridgeModule(module, state.platforms)) { + return; + } + + const config = state.moduleConfigs[module.id] || {}; + + if (module.id === "logging") { + imports.add("ru.bartwell.kick.module.logging.LoggingModule"); + const useCustomExtractor = config.labelExtractor === "custom"; + if (useCustomExtractor) { + imports.add("ru.bartwell.kick.module.logging.feature.table.util.LabelExtractor"); + helperFunctions.push( + " private fun customLoggingLabelExtractor(): LabelExtractor = object : LabelExtractor {", + " override fun extract(message: String?): Set {", + " return emptySet()", + " }", + " }" + ); + moduleLines.push(" module(LoggingModule(context, customLoggingLabelExtractor()))"); + } else { + imports.add("ru.bartwell.kick.module.logging.feature.table.util.BracketLabelExtractor"); + moduleLines.push(" module(LoggingModule(context, BracketLabelExtractor()))"); + } + if (config.integrateNapier === true) { + hasNapierBridge = true; + } + return; + } + + if (module.id === "ktor3") { + imports.add("ru.bartwell.kick.module.ktor3.Ktor3Module"); + moduleLines.push(" module(Ktor3Module(context))"); + moduleLines.push(" // Ktor client integration (outside Kick.init):"); + moduleLines.push(" // install(KickKtor3Plugin) {"); + moduleLines.push(" // maxBodySizeBytes = 1024 * 1024L"); + moduleLines.push(" // }"); + return; + } + + if (module.id === "control_panel") { + imports.add("ru.bartwell.kick.module.controlpanel.ControlPanelModule"); + imports.add("ru.bartwell.kick.module.controlpanel.data.ControlPanelItem"); + imports.add("ru.bartwell.kick.module.controlpanel.data.InputType"); + imports.add("ru.bartwell.kick.module.controlpanel.data.ActionType"); + imports.add("ru.bartwell.kick.module.controlpanel.data.Editor"); + moduleLines.push(" module(ControlPanelModule(context = context, items = buildControlPanelItems()))"); + helperFunctions.push(...buildControlPanelItemsFunction(config)); + return; + } + + if (module.id === "sqldelight") { + imports.add("ru.bartwell.kick.module.sqlite.runtime.SqliteModule"); + moduleLines.push(" deps.sqlDelightWrapper?.let { wrapper ->"); + moduleLines.push(" module(SqliteModule(wrapper))"); + moduleLines.push(" }"); + return; + } + + if (module.id === "room") { + imports.add("ru.bartwell.kick.module.sqlite.runtime.SqliteModule"); + moduleLines.push(" deps.roomWrapper?.let { wrapper ->"); + moduleLines.push(" module(SqliteModule(wrapper))"); + moduleLines.push(" }"); + return; + } + + if (module.id === "multiplatform_settings") { + const storageNames = normalizeSettingsConfig(config); + imports.add("com.russhwolf.settings.Settings"); + imports.add("ru.bartwell.kick.module.multiplatformsettings.MultiplatformSettingsModule"); + depLines.push(" // Provide Settings instances for storages listed below (leave null to skip)."); + storageNames.forEach((storageName, index) => { + depLines.push(` val settingsStorage${index + 1}: Settings? = null, // ${escapeKotlinString(storageName)}`); + }); + preInitLines.push(" val settingsStorages = buildList> {"); + storageNames.forEach((storageName, index) => { + preInitLines.push(` deps.settingsStorage${index + 1}?.let { add(\"${escapeKotlinString(storageName)}\" to it) }`); + }); + preInitLines.push(" }"); + moduleLines.push(" module(MultiplatformSettingsModule(settingsStorages))"); + return; + } + + if (module.id === "file_explorer") { + imports.add("ru.bartwell.kick.module.explorer.FileExplorerModule"); + moduleLines.push(" module(FileExplorerModule())"); + return; + } + + if (module.id === "layout") { + imports.add("ru.bartwell.kick.module.layout.LayoutModule"); + moduleLines.push(" module(LayoutModule(context))"); + return; + } + + if (module.id === "overlay") { + imports.add("ru.bartwell.kick.module.overlay.OverlayModule"); + const performance = config.enablePerformanceProvider !== false; + if (performance) { + moduleLines.push(" module(OverlayModule(context))"); + } else { + moduleLines.push(" module(OverlayModule(context = context, providers = emptyList()))"); + } + return; + } + + if (module.id === "runner") { + imports.add("ru.bartwell.kick.module.runner.RunnerModule"); + moduleLines.push(" module(RunnerModule())"); + if (config.generateSampleCalls === true) { + hasRunnerSamples = true; + imports.add("ru.bartwell.kick.module.runner.runner"); + imports.add("ru.bartwell.kick.module.runner.core.renderer.JsonRunnerRenderer"); + } + return; + } + + if (module.id === "firebase_cloud_messaging") { + imports.add("ru.bartwell.kick.module.firebase.cloudmessaging.FirebaseCloudMessagingModule"); + moduleLines.push(" module(FirebaseCloudMessagingModule(context))"); + return; + } + + if (module.id === "firebase_analytics") { + imports.add("ru.bartwell.kick.module.firebase.analytics.FirebaseAnalyticsModule"); + moduleLines.push(" module(FirebaseAnalyticsModule(context))"); + } + }); + + if (hasNapierBridge) { + imports.add("io.github.aakira.napier.Antilog"); + imports.add("io.github.aakira.napier.Napier"); + imports.add("ru.bartwell.kick.module.logging.core.data.LogLevel"); + imports.add("ru.bartwell.kick.module.logging.log"); + imports.add("io.github.aakira.napier.LogLevel as NapierLogLevel"); + + postInitLines.unshift(" installNapierBridge()"); + helperFunctions.push( + " private fun installNapierBridge() {", + " Napier.base(object : Antilog() {", + " override fun performLog(priority: NapierLogLevel, tag: String?, throwable: Throwable?, message: String?) {", + " val level = when (priority) {", + " NapierLogLevel.VERBOSE -> LogLevel.VERBOSE", + " NapierLogLevel.DEBUG -> LogLevel.DEBUG", + " NapierLogLevel.INFO -> LogLevel.INFO", + " NapierLogLevel.WARNING -> LogLevel.WARNING", + " NapierLogLevel.ERROR -> LogLevel.ERROR", + " NapierLogLevel.ASSERT -> LogLevel.ASSERT", + " }", + " Kick.log(level, message)", + " }", + " })", + " }" + ); + } + + if (hasRunnerSamples) { + postInitLines.push(" registerRunnerSamples()"); + helperFunctions.push( + " private fun registerRunnerSamples() {", + " Kick.runner.addCall(", + " title = \"Sample JSON\",", + " description = \"Generated by Kick Wizard\",", + " renderer = JsonRunnerRenderer(),", + " ) {", + " \"{\\\"status\\\":\\\"ok\\\",\\\"source\\\":\\\"kick-wizard\\\"}\"", + " }", + " }" + ); + } + + if (hasPlatformBridge) { + moduleLines.push(" installPlatformKickModules(context, deps)"); + } + + const dedupDepLines = unique(depLines); + const sortedImports = Array.from(imports).sort(); + + const code = []; + code.push(sortedImports.map((entry) => `import ${entry}`).join("\n")); + code.push(""); + + if (dedupDepLines.length > 0) { + code.push("data class KickDeps("); + dedupDepLines.forEach((line) => code.push(line)); + code.push(")"); + } else { + code.push("data class KickDeps()"); + } + + code.push(""); + code.push("object KickBootstrap {"); + code.push(" fun init(context: PlatformContext, deps: KickDeps) {"); + + if (preInitLines.length > 0) { + preInitLines.forEach((line) => code.push(line)); + } + + code.push(" Kick.init(context) {"); + code.push(" enableShortcut = true"); + + if (moduleLines.length === 0) { + code.push(" // Select at least one module in wizard."); + } else { + moduleLines.forEach((line) => code.push(line)); + } + + code.push(" }"); + + if (postInitLines.length > 0) { + postInitLines.forEach((line) => code.push(line)); + } + + code.push(" }"); + code.push(""); + code.push(" fun launch(context: PlatformContext) {"); + code.push(" Kick.launch(context)"); + code.push(" }"); + + if (helperFunctions.length > 0) { + code.push(""); + helperFunctions.forEach((line) => code.push(line)); + } + + code.push("}"); + + if (hasPlatformBridge) { + code.push(""); + code.push("internal expect fun Kick.Configuration.installPlatformKickModules("); + code.push(" context: PlatformContext,"); + code.push(" deps: KickDeps,"); + code.push(")"); + } + + return code.join("\n"); +} + +function buildPlatformBridgeActualFile(platform, bridgeModules) { + const imports = new Set([ + "ru.bartwell.kick.Kick", + "ru.bartwell.kick.core.data.PlatformContext", + ]); + + const bodyLines = []; + + bridgeModules.forEach((module) => { + if (!isPlatformSupported(module, platform)) { + return; + } + + if (module.id === "room") { + imports.add("ru.bartwell.kick.module.sqlite.runtime.SqliteModule"); + bodyLines.push(" deps.roomWrapper?.let { wrapper ->"); + bodyLines.push(" module(SqliteModule(wrapper))"); + bodyLines.push(" }"); + return; + } + + if (module.id === "layout") { + imports.add("ru.bartwell.kick.module.layout.LayoutModule"); + bodyLines.push(" module(LayoutModule(context))"); + return; + } + + if (module.id === "firebase_cloud_messaging") { + imports.add("ru.bartwell.kick.module.firebase.cloudmessaging.FirebaseCloudMessagingModule"); + bodyLines.push(" module(FirebaseCloudMessagingModule(context))"); + return; + } + + if (module.id === "firebase_analytics") { + imports.add("ru.bartwell.kick.module.firebase.analytics.FirebaseAnalyticsModule"); + bodyLines.push(" module(FirebaseAnalyticsModule(context))"); + } + }); + + if (bodyLines.length === 0) { + bodyLines.push(" // No platform-specific Kick modules for this platform."); + } + + const lines = []; + lines.push(Array.from(imports).sort().map((entry) => `import ${entry}`).join("\n")); + lines.push(""); + lines.push("internal actual fun Kick.Configuration.installPlatformKickModules(context: PlatformContext, deps: KickDeps) {"); + bodyLines.forEach((line) => lines.push(line)); + lines.push("}"); + + const path = PLATFORM_FILE_PATHS[platform]; + const fileName = path.split("/").pop() || path; + + return { + path, + title: `${fileName} (${platform})`, + code: lines.join("\n"), + }; +} + +function buildPlatformBridgeFiles(state, selectedModules) { + const bridgeModules = selectedModules.filter((module) => requiresPlatformBridgeModule(module, state.platforms)); + if (bridgeModules.length === 0) { + return []; + } + + const files = []; + state.platforms + .filter((platform) => PLATFORM_ORDER.includes(platform)) + .forEach((platform) => { + files.push(buildPlatformBridgeActualFile(platform, bridgeModules)); + }); + + return files; +} + +function buildGlueGuideItems(state, selectedModules) { + const selectedIds = new Set(selectedModules.map((module) => module.id)); + const items = []; + + if (selectedIds.has("logging")) { + const loggingConfig = (state.moduleConfigs && state.moduleConfigs.logging) || {}; + if (loggingConfig.integrateNapier !== true) { + items.push({ + type: "logging", + integrateNapier: false, + }); + } + } + + if (selectedIds.has("ktor3")) { + items.push({ type: "ktor3" }); + } + + if (selectedIds.has("firebase_cloud_messaging")) { + items.push({ + type: "firebase_cloud_messaging", + includeAndroid: state.platforms.includes("android"), + includeIos: state.platforms.includes("ios"), + }); + } + + if (selectedIds.has("firebase_analytics")) { + items.push({ type: "firebase_analytics" }); + } + + if (selectedIds.has("control_panel")) { + items.push({ type: "control_panel" }); + } + + if (selectedIds.has("overlay")) { + items.push({ type: "overlay" }); + } + + if (selectedIds.has("runner")) { + items.push({ type: "runner" }); + } + + return items; +} + +export function shouldShowPlatformGlue(state, modulesById) { + const selectedModules = getSelectedModules(state, modulesById); + const files = buildPlatformBridgeFiles(state, selectedModules); + const guideItems = buildGlueGuideItems(state, selectedModules); + return files.length > 0 || guideItems.length > 0; +} + +export function buildOutput(state, modulesById, kickVersion) { + const selectedModules = getSelectedModules(state, modulesById); + const glueFiles = buildPlatformBridgeFiles(state, selectedModules); + const glueGuideItems = buildGlueGuideItems(state, selectedModules); + + return { + gradle: buildGradleSnippet(selectedModules, kickVersion), + commonKotlin: buildCommonSnippet(state, selectedModules, glueFiles.length > 0), + glue: { + required: glueFiles.length > 0 || glueGuideItems.length > 0, + files: glueFiles, + guideItems: glueGuideItems, + }, + }; +} diff --git a/content/wizard/app/router.js b/content/wizard/app/router.js new file mode 100644 index 00000000..f50a9d64 --- /dev/null +++ b/content/wizard/app/router.js @@ -0,0 +1,134 @@ +const ROOT_ROUTE = "/"; +const PLATFORMS_ROUTE = "/platforms"; +const MODULES_ROUTE = "/modules"; +const OUTPUT_ROUTE = "/output"; +const MODULE_PREFIX = "/module/"; + +export function getRoutes() { + return { + root: ROOT_ROUTE, + platforms: PLATFORMS_ROUTE, + modules: MODULES_ROUTE, + output: OUTPUT_ROUTE, + modulePrefix: MODULE_PREFIX, + }; +} + +export function toModuleRoute(moduleId) { + return `${MODULE_PREFIX}${moduleId}`; +} + +export function parseCurrentRoute() { + const hash = window.location.hash.replace(/^#/, "") || ROOT_ROUTE; + const normalized = hash.startsWith("/") ? hash : `/${hash}`; + if (normalized.startsWith(MODULE_PREFIX)) { + const moduleId = normalized.slice(MODULE_PREFIX.length).trim(); + return { + route: toModuleRoute(moduleId), + type: "module", + moduleId, + }; + } + + if ([ROOT_ROUTE, PLATFORMS_ROUTE, MODULES_ROUTE, OUTPUT_ROUTE].includes(normalized)) { + return { + route: normalized, + type: "static", + moduleId: null, + }; + } + + return { + route: ROOT_ROUTE, + type: "static", + moduleId: null, + }; +} + +export function getWizardSteps(state) { + const moduleFlowIds = Array.isArray(state.moduleFlowIds) + ? state.moduleFlowIds + : state.selectedModules; + const moduleRoutes = moduleFlowIds.map((moduleId) => toModuleRoute(moduleId)); + return [ROOT_ROUTE, PLATFORMS_ROUTE, MODULES_ROUTE, ...moduleRoutes, OUTPUT_ROUTE]; +} + +export function getStepPosition(route, state) { + const steps = getWizardSteps(state); + const index = steps.indexOf(route); + if (index < 0) { + return { + index: 0, + total: steps.length, + steps, + }; + } + return { + index, + total: steps.length, + steps, + }; +} + +export function getAdjacentRoutes(route, state) { + const { index, steps } = getStepPosition(route, state); + return { + prev: index > 0 ? steps[index - 1] : null, + next: index < steps.length - 1 ? steps[index + 1] : null, + }; +} + +export function ensureRouteAllowed(routeInfo, state) { + const hasPlatforms = state.platforms.length > 0; + const hasModules = state.selectedModules.length > 0; + const moduleFlowIds = Array.isArray(state.moduleFlowIds) + ? state.moduleFlowIds + : state.selectedModules; + + if (routeInfo.route === ROOT_ROUTE) { + return ROOT_ROUTE; + } + + if (!hasPlatforms) { + return PLATFORMS_ROUTE; + } + + if (routeInfo.route === PLATFORMS_ROUTE) { + return PLATFORMS_ROUTE; + } + + if (routeInfo.route === MODULES_ROUTE) { + return MODULES_ROUTE; + } + + if (!hasModules) { + return MODULES_ROUTE; + } + + if (routeInfo.type === "module") { + if (routeInfo.moduleId && moduleFlowIds.includes(routeInfo.moduleId)) { + return routeInfo.route; + } + if (moduleFlowIds.length > 0) { + return toModuleRoute(moduleFlowIds[0]); + } + return OUTPUT_ROUTE; + } + + if (routeInfo.route === OUTPUT_ROUTE) { + return OUTPUT_ROUTE; + } + + return ROOT_ROUTE; +} + +export function navigateTo(route, { replace = false } = {}) { + const target = `#${route}`; + if (replace) { + const url = new URL(window.location.href); + url.hash = target; + window.history.replaceState({}, "", url.toString()); + return; + } + window.location.hash = target; +} diff --git a/content/wizard/app/state.js b/content/wizard/app/state.js new file mode 100644 index 00000000..8faab270 --- /dev/null +++ b/content/wizard/app/state.js @@ -0,0 +1,129 @@ +export const STORAGE_KEY = "kick.wizard.state.v1"; +export const URL_STATE_KEY = "state"; + +const EMPTY_OBJECT = Object.freeze({}); + +function clone(value) { + return JSON.parse(JSON.stringify(value)); +} + +function safeArray(value) { + return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : []; +} + +function toUrlSafeBase64(input) { + const raw = btoa(unescape(encodeURIComponent(input))); + return raw.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, ""); +} + +function fromUrlSafeBase64(input) { + const normalized = input.replace(/-/g, "+").replace(/_/g, "/"); + const padding = normalized.length % 4 === 0 ? "" : "=".repeat(4 - (normalized.length % 4)); + const decoded = atob(`${normalized}${padding}`); + return decodeURIComponent(escape(decoded)); +} + +export function createDefaultState() { + return { + lang: "en", + platforms: [], + selectedModules: [], + moduleConfigs: {}, + }; +} + +export function hydrateState(rawState = EMPTY_OBJECT) { + const base = createDefaultState(); + const state = { + ...base, + ...rawState, + platforms: safeArray(rawState.platforms), + selectedModules: safeArray(rawState.selectedModules), + moduleConfigs: rawState.moduleConfigs && typeof rawState.moduleConfigs === "object" + ? clone(rawState.moduleConfigs) + : {}, + }; + if (typeof rawState.lang === "string") { + state.lang = rawState.lang; + } + return state; +} + +export function loadStateFromStorage() { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) { + return null; + } + const parsed = JSON.parse(raw); + return hydrateState(parsed); + } catch (_error) { + return null; + } +} + +export function saveStateToStorage(state) { + const payload = { + lang: state.lang, + platforms: state.platforms, + selectedModules: state.selectedModules, + moduleConfigs: state.moduleConfigs, + }; + localStorage.setItem(STORAGE_KEY, JSON.stringify(payload)); +} + +export function clearStateStorage() { + localStorage.removeItem(STORAGE_KEY); +} + +export function loadStateFromUrl() { + const url = new URL(window.location.href); + const encodedState = url.searchParams.get(URL_STATE_KEY); + if (!encodedState) { + return null; + } + try { + const json = fromUrlSafeBase64(encodedState); + const parsed = JSON.parse(json); + return hydrateState(parsed); + } catch (_error) { + return null; + } +} + +export function getLanguageFromUrl() { + const url = new URL(window.location.href); + return url.searchParams.get("lang"); +} + +function buildShareState(state) { + return { + platforms: state.platforms, + selectedModules: state.selectedModules, + moduleConfigs: state.moduleConfigs, + }; +} + +function isShareStateEmpty(state) { + return state.platforms.length === 0 && state.selectedModules.length === 0; +} + +export function saveUrlState(state) { + const url = new URL(window.location.href); + const shareState = buildShareState(state); + + if (isShareStateEmpty(shareState)) { + url.searchParams.delete(URL_STATE_KEY); + } else { + url.searchParams.set(URL_STATE_KEY, toUrlSafeBase64(JSON.stringify(shareState))); + } + + url.searchParams.set("lang", state.lang); + window.history.replaceState({}, "", url.toString()); +} + +export function clearUrlState() { + const url = new URL(window.location.href); + url.searchParams.delete(URL_STATE_KEY); + window.history.replaceState({}, "", url.toString()); +} diff --git a/content/wizard/data/modules.json b/content/wizard/data/modules.json new file mode 100644 index 00000000..2a097735 --- /dev/null +++ b/content/wizard/data/modules.json @@ -0,0 +1,194 @@ +[ + { + "id": "sqldelight", + "titleKey": "module.sqldelight.title", + "descriptionKey": "module.sqldelight.description", + "kickModuleEnum": "KickModule.SqliteRuntime", + "extraKickModuleEnums": ["KickModule.SqliteSqlDelightAdapter"], + "supportedPlatforms": ["android", "ios", "jvm", "wasm"], + "needsPlatformGlue": false, + "configComplexity": "medium", + "configPages": [], + "outputSnippets": ["gradle.sqliteRuntime", "gradle.sqliteSqlDelightAdapter", "common.sqliteSqlDelight"], + "manualArtifacts": { + "runtime": ["sqlite-core", "sqlite-runtime", "sqlite-sqldelight-adapter"], + "stub": ["sqlite-runtime-stub", "sqlite-sqldelight-adapter-stub"] + } + }, + { + "id": "room", + "titleKey": "module.room.title", + "descriptionKey": "module.room.description", + "kickModuleEnum": "KickModule.SqliteRuntime", + "extraKickModuleEnums": ["KickModule.SqliteRoomAdapter"], + "supportedPlatforms": ["android", "ios", "jvm"], + "needsPlatformGlue": false, + "configComplexity": "medium", + "configPages": [], + "outputSnippets": ["gradle.sqliteRuntime", "gradle.sqliteRoomAdapter", "common.sqliteRoom"], + "manualArtifacts": { + "runtime": ["sqlite-core", "sqlite-runtime", "sqlite-room-adapter"], + "stub": ["sqlite-runtime-stub", "sqlite-room-adapter-stub"] + } + }, + { + "id": "logging", + "titleKey": "module.logging.title", + "descriptionKey": "module.logging.description", + "kickModuleEnum": "KickModule.Logging", + "extraKickModuleEnums": [], + "supportedPlatforms": ["android", "ios", "jvm", "wasm"], + "needsPlatformGlue": false, + "configComplexity": "low", + "configPages": ["logging"], + "outputSnippets": ["gradle.logging", "common.logging"], + "manualArtifacts": { + "runtime": ["logging"], + "stub": ["logging-stub"] + } + }, + { + "id": "ktor3", + "titleKey": "module.ktor3.title", + "descriptionKey": "module.ktor3.description", + "kickModuleEnum": "KickModule.Ktor3", + "extraKickModuleEnums": [], + "supportedPlatforms": ["android", "ios", "jvm", "wasm"], + "needsPlatformGlue": false, + "configComplexity": "low", + "configPages": [], + "outputSnippets": ["gradle.ktor3", "common.ktor3"], + "manualArtifacts": { + "runtime": ["ktor3"], + "stub": ["ktor3-stub"] + } + }, + { + "id": "control_panel", + "titleKey": "module.control_panel.title", + "descriptionKey": "module.control_panel.description", + "kickModuleEnum": null, + "extraKickModuleEnums": [], + "supportedPlatforms": ["android", "ios", "jvm", "wasm"], + "needsPlatformGlue": false, + "configComplexity": "high", + "configPages": ["control_panel"], + "outputSnippets": ["gradle.manualControlPanel", "common.controlPanel"], + "manualArtifacts": { + "runtime": ["control-panel"], + "stub": ["control-panel-stub"] + } + }, + { + "id": "multiplatform_settings", + "titleKey": "module.multiplatform_settings.title", + "descriptionKey": "module.multiplatform_settings.description", + "kickModuleEnum": "KickModule.MultiplatformSettings", + "extraKickModuleEnums": [], + "supportedPlatforms": ["android", "ios", "jvm", "wasm"], + "needsPlatformGlue": false, + "configComplexity": "low", + "configPages": ["multiplatform_settings"], + "outputSnippets": ["gradle.multiplatformSettings", "common.multiplatformSettings"], + "manualArtifacts": { + "runtime": ["multiplatform-settings"], + "stub": ["multiplatform-settings-stub"] + } + }, + { + "id": "file_explorer", + "titleKey": "module.file_explorer.title", + "descriptionKey": "module.file_explorer.description", + "kickModuleEnum": "KickModule.FileExplorer", + "extraKickModuleEnums": [], + "supportedPlatforms": ["android", "ios", "jvm", "wasm"], + "needsPlatformGlue": false, + "configComplexity": "low", + "configPages": [], + "outputSnippets": ["gradle.fileExplorer", "common.fileExplorer"], + "manualArtifacts": { + "runtime": ["file-explorer"], + "stub": ["file-explorer-stub"] + } + }, + { + "id": "layout", + "titleKey": "module.layout.title", + "descriptionKey": "module.layout.description", + "kickModuleEnum": "KickModule.Layout", + "extraKickModuleEnums": [], + "supportedPlatforms": ["android", "ios", "jvm"], + "needsPlatformGlue": false, + "configComplexity": "medium", + "configPages": [], + "outputSnippets": ["gradle.layout", "common.layout"], + "manualArtifacts": { + "runtime": ["layout"], + "stub": ["layout-stub"] + } + }, + { + "id": "overlay", + "titleKey": "module.overlay.title", + "descriptionKey": "module.overlay.description", + "kickModuleEnum": null, + "extraKickModuleEnums": [], + "supportedPlatforms": ["android", "ios", "jvm", "wasm"], + "needsPlatformGlue": false, + "configComplexity": "medium", + "configPages": ["overlay"], + "outputSnippets": ["gradle.manualOverlay", "common.overlay"], + "manualArtifacts": { + "runtime": ["overlay"], + "stub": ["overlay-stub"] + } + }, + { + "id": "firebase_cloud_messaging", + "titleKey": "module.firebase_cloud_messaging.title", + "descriptionKey": "module.firebase_cloud_messaging.description", + "kickModuleEnum": "KickModule.FirebaseCloudMessaging", + "extraKickModuleEnums": [], + "supportedPlatforms": ["android", "ios"], + "needsPlatformGlue": true, + "configComplexity": "medium", + "configPages": [], + "outputSnippets": ["gradle.firebaseCloudMessaging", "common.firebaseCloudMessaging", "glue.firebaseCloudMessaging"], + "manualArtifacts": { + "runtime": ["firebase-cloud-messaging"], + "stub": ["firebase-cloud-messaging-stub"] + } + }, + { + "id": "firebase_analytics", + "titleKey": "module.firebase_analytics.title", + "descriptionKey": "module.firebase_analytics.description", + "kickModuleEnum": null, + "extraKickModuleEnums": [], + "supportedPlatforms": ["android", "ios"], + "needsPlatformGlue": true, + "configComplexity": "medium", + "configPages": [], + "outputSnippets": ["gradle.manualFirebaseAnalytics", "common.firebaseAnalytics", "glue.firebaseAnalytics"], + "manualArtifacts": { + "runtime": ["firebase-analytics"], + "stub": ["firebase-analytics-stub"] + } + }, + { + "id": "runner", + "titleKey": "module.runner.title", + "descriptionKey": "module.runner.description", + "kickModuleEnum": null, + "extraKickModuleEnums": [], + "supportedPlatforms": ["android", "ios", "jvm", "wasm"], + "needsPlatformGlue": false, + "configComplexity": "low", + "configPages": ["runner"], + "outputSnippets": ["gradle.manualRunner", "common.runner"], + "manualArtifacts": { + "runtime": ["runner"], + "stub": ["runner-stub"] + } + } +] diff --git a/content/wizard/data/version.json b/content/wizard/data/version.json new file mode 100644 index 00000000..9529f85e --- /dev/null +++ b/content/wizard/data/version.json @@ -0,0 +1,3 @@ +{ + "kickVersion": "2.2.0" +} diff --git a/content/wizard/favicon.ico b/content/wizard/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..cd71924c7871e85cbf138726c825c482ec4ffef7 GIT binary patch literal 4286 zcmc(jTXWJt6oA+92PkSw!Pdf?KO_IZTK|aHN?UGCp#+MT+IlUtf-l}iXY@^}Lr`mV z)L+ozgIeExa-8wucuvaJO~`Jb1q^dY2$%26?%A^&7(0ZIP>9hpdw7(wLB`l=3_Qg& z3_4Gnj_~o{4P2GIzX-x*4#YeR|00@Y5>pydhT#9 zY>E!?%R?Flpab;HL1>5AC1ex7&kocEGX=jMD-1!m9n8-KpvcH`lJY^HS@4_a05XR^ z@C$gn`jGd`$;$I{d!MK8c{rHUc|V{7$Q(GE_7-0;E@~eaZ0tQgjZaBCOy>qc;lSsyIE!l8xls1IvT$6L7co!X~DTQzFR!0RWz#KFej%-iY%^@sRA zKQ2ETZ@W%TWd}gzz<-nW>%?!?sK$4*h27XOJFeAH;wv2N{2Y`XkF{{=Gkk1xI$uQR z2SDW@^0&X$T-mEn&hC4@h-Q$la1i;<<8ADNgU%Nh(zlc*%ooI0IdGnH zZ}Gb@Urf&J@?{QAuK1er;h=NYq<$P!4kB;st9$;H9*yn0FD7P@uW%6g)wc$$+&|+w z2KJi|#8){u`MgeUxr{}tKRR5)Or(xke3^sDfBD9+)#~bo;o25dqj;V4TFYHXB46R) zYPHIN_$mjH{{pXHR;%6n1M8E=nl_T~S$vs;ldr1PZv7$7S#IqRiTg~I zgN^^%P}XW%M-?B~$A`zbFp;lgfXYEtADsEX*K*a398}+7jaICy={}3rz>yg8d*C2j z)ABis*tDIG`LzGwpjq(d6FQiEbg|9v ZPsWba89Pj&)87R>+UU3IO%5@M{TG(_Q4RnA literal 0 HcmV?d00001 diff --git a/content/wizard/i18n/en.json b/content/wizard/i18n/en.json new file mode 100644 index 00000000..f1a3ec03 --- /dev/null +++ b/content/wizard/i18n/en.json @@ -0,0 +1,274 @@ +{ + "app": { + "pageTitle": "Kick Integration Wizard", + "wizardTitle": "Integration Wizard" + }, + "language": { + "label": "Language", + "en": "English", + "ru": "Russian", + "es": "Spanish", + "pt": "Portuguese", + "zh-Hans": "Chinese (Simplified)", + "ja": "Japanese" + }, + "nav": { + "back": "Back", + "next": "Next", + "nextModule": "Next", + "done": "Done", + "reset": "Reset", + "resetConfirm": "Reset wizard state and clear saved selections?" + }, + "progress": { + "step": "Step {current} of {total}" + }, + "summary": { + "platforms": "Platforms", + "modules": "Modules", + "version": "Kick Version", + "count": "{count} selected" + }, + "step": { + "welcome": { + "title": "Generate Kick integration in minutes", + "subtitle": "Choose language, platforms, modules, and produce ready-to-paste Gradle + Kotlin snippets.", + "checklistTitle": "Wizard flow", + "checklistSubtitle": "The output page combines all choices into one integration draft.", + "itemLanguage": "Select UI language", + "itemPlatforms": "Select app platforms", + "itemModules": "Select modules and configure each selected module", + "itemOutput": "Copy Gradle, Common Kotlin, and optional Platform glue" + }, + "platforms": { + "title": "Select Platforms", + "subtitle": "Choose all platforms currently present in your app." + }, + "modules": { + "title": "Select Modules", + "subtitle": "Pick modules to include in Kick initialization.", + "selectedCount": "Selected modules: {count}" + }, + "moduleConfig": { + "title": "Configure: {module}" + }, + "output": { + "title": "Integration Output", + "subtitle": "Copy snippets and paste them into your project." + } + }, + "platform": { + "android": "Android", + "ios": "iOS", + "jvm": "JVM (Desktop)", + "wasm": "WasmJS", + "androidDescription": "Android application target", + "iosDescription": "iOS application target", + "jvmDescription": "Desktop JVM target", + "wasmDescription": "WebAssembly JS target" + }, + "validation": { + "platformRequired": "Select at least one platform before moving forward.", + "moduleRequired": "Select at least one module before moving forward." + }, + "modules": { + "unsupportedWarning": "Not supported on: {platforms}", + "unsupportedFallback": "Suggested fallback: {fallback}" + }, + "badge": { + "highConfiguration": "High configuration" + }, + "config": { + "advanced": "Advanced", + "noQuestions": "No additional questions for this module in the default flow.", + "databaseCount": "Number of databases", + "databaseNameLabel": "Display name #{index}", + "displayName": "Display name", + "storageItem": "Storage #{index}", + "storageDefaultName": "Storage", + "logging": { + "integrateNapier": "Integrate with Napier", + "labelExtractorBracket": "Use BracketLabelExtractor", + "labelExtractorCustom": "Use custom label extractor stub" + }, + "ktor3": { + "showBodies": "Show request/response bodies", + "redactionKeys": "Redaction keys (comma or new lines)" + }, + "overlay": { + "enablePerformanceProvider": "Enable performance provider", + "customProviders": "Custom providers (comma or new lines)" + }, + "runner": { + "generateSampleCalls": "Generate sample calls" + }, + "controlPanel": { + "item": "Item #{index}", + "name": "Name", + "typeLabel": "Type", + "category": "Category (optional)", + "editorLabel": "Editor", + "listValues": "List values (comma separated)", + "type": { + "bool": "Bool", + "int": "Int", + "string": "String", + "list": "List", + "button": "Button" + }, + "editor": { + "none": "None", + "input_number": "Number input", + "input_string": "String input", + "list": "List editor" + } + } + }, + "actions": { + "copy": "Copy", + "remove": "Remove", + "skipModule": "Don't add module", + "addStorage": "Add storage", + "addItem": "Add item", + "addExampleItems": "Add example items" + }, + "output": { + "steps": { + "gradle": "Add Kick plugin to your shared build.gradle.kts:", + "file": "Create {path} with this content:", + "ktorExampleTitle": "Ktor3 integration example", + "ktorExampleDescription": "Add this when creating your HttpClient.", + "fcmExampleTitle": "Firebase Cloud Messaging example", + "fcmExampleDescription": "Forward push payloads to Kick in your platform callbacks.", + "analyticsExampleTitle": "Firebase Analytics example", + "analyticsExampleDescription": "Call Kick analytics methods in the same wrapper where you call Firebase SDK.", + "controlPanelExampleTitle": "Control Panel usage example", + "controlPanelExampleDescription": "Read Control Panel values and execute business logic with them.", + "overlayExampleTitle": "Overlay updates example", + "overlayExampleDescription": "Push live values to Overlay from any place in your app.", + "runnerExampleTitle": "Runner call example", + "runnerExampleDescription": "Register a debug action and return JSON for inspection." + }, + "gradle": { + "title": "Gradle", + "description": "Kick plugin setup with selected modules and overrides." + }, + "common": { + "title": "Common Kotlin", + "description": "Create shared/src/commonMain/kotlin/KickBootstrap.kt and paste this code." + }, + "platformGlue": { + "title": "Platform glue", + "description": "Follow this checklist and add platform files below when they are generated.", + "pathLabel": "Put this file at:", + "noFiles": "No platform glue files are required for the current selection." + } + }, + "glue": { + "init": { + "android": "Call KickBootstrap.init(...) during app startup (Application.onCreate or equivalent).", + "ios": "Call KickBootstrap.init(...) from shared startup path used by iOS app lifecycle.", + "jvm": "Call KickBootstrap.init(...) before your desktop window is shown.", + "wasm": "Call KickBootstrap.init(...) early in your Wasm app startup." + }, + "hook": { + "firebaseCloudMessagingAndroid": "In FirebaseMessagingService.onMessageReceived(...) call Kick.firebaseCloudMessaging.handleFcm(message).", + "firebaseCloudMessagingIos": "When APNS notification is received, call one of Kick.firebaseCloudMessaging.handleApns... handlers from shared Kotlin API bridge.", + "firebaseAnalytics": "Mirror analytics wrapper calls to Kick.firebaseAnalytics.logEvent / setUserId / setUserProperty." + }, + "unsupportedModule": "{module}: unsupported on this platform.", + "analytics": { + "title": "Firebase Analytics quick setup", + "subtitle": "Follow these steps in order. Do exactly this and integration will be consistent.", + "step1": "Create one shared wrapper file: shared/src/commonMain/kotlin/AnalyticsReporter.kt.", + "step2": "In logEvent(...), call Firebase SDK first, then call Kick.firebaseAnalytics.logEvent(name, params).", + "step3": "In setUserId(...), call Firebase SDK first, then call Kick.firebaseAnalytics.setUserId(id).", + "step4": "In setUserProperty(...), call Firebase SDK first, then call Kick.firebaseAnalytics.setUserProperty(name, value).", + "step5": "Replace direct Firebase calls with this wrapper everywhere in the app." + }, + "guide": { + "title": "What to wire in your app code", + "subtitle": "Follow these steps. Only relevant items are shown for your selected modules.", + "logging": "To send logs to Logging module, call Kick.log(level, message) in the places you want to log.", + "loggingWithNapier": "To send logs to Logging module, call Kick.log(level, message) where needed, or route all Napier logs with installNapierBridge() from KickBootstrap.", + "ktor3": "To log Ktor3 network activity, add install(KickKtor3Plugin) when you create HttpClient.", + "firebaseCloudMessagingBoth": "For Firebase Cloud Messaging: on Android call Kick.firebaseCloudMessaging.handleFcm(message) inside FirebaseMessagingService.onMessageReceived(...). On iOS call KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo) in AppDelegate when APNS payload is received.", + "firebaseCloudMessagingAndroid": "For Firebase Cloud Messaging on Android: call Kick.firebaseCloudMessaging.handleFcm(message) inside FirebaseMessagingService.onMessageReceived(...).", + "firebaseCloudMessagingIos": "For Firebase Cloud Messaging on iOS: call KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo) in AppDelegate when APNS payload is received.", + "firebaseCloudMessagingGeneric": "For Firebase Cloud Messaging, forward push payloads to Kick handlers in your platform lifecycle code.", + "firebaseAnalytics": "For Firebase Analytics, add Kick.firebaseAnalytics.logEvent(...), Kick.firebaseAnalytics.setUserId(...), and Kick.firebaseAnalytics.setUserProperty(...) in matching places in your analytics wrapper.", + "controlPanel": "Read ControlPanel values in your code with Kick.controlPanel.getBoolean(...), Kick.controlPanel.getString(...), etc. If you added buttons, listen for clicks with Kick.controlPanel.events.collect { event -> ... }.", + "overlay": "Show required runtime values in Overlay floating window with Kick.overlay.set(\"key\", value).", + "runner": "Register useful debug actions in the places you need with Kick.runner.addCall(...)." + } + }, + "toast": { + "copied": "Copied", + "copyFailed": "Copy failed" + }, + "errors": { + "loadFailed": "Failed to load wizard resources." + }, + "module": { + "sqldelight": { + "title": "SQLDelight", + "description": "View and edit SQLDelight databases.", + "fallback": "keep SQLDelight only on supported targets or use stub/no-op in unsupported builds" + }, + "room": { + "title": "Room", + "description": "Inspect Room database tables and values.", + "fallback": "use SQLDelight on Web targets or provide no-op fallback for Room" + }, + "logging": { + "title": "Logging", + "description": "Live log viewer with filtering support.", + "fallback": "disable module on unsupported targets or use stub/no-op" + }, + "ktor3": { + "title": "Ktor3", + "description": "Network monitor for Ktor requests and responses.", + "fallback": "disable module on unsupported targets or use stub/no-op" + }, + "control_panel": { + "title": "Control Panel", + "description": "Runtime feature toggles and editable debug values.", + "fallback": "disable module on unsupported targets or use control-panel-stub" + }, + "multiplatform_settings": { + "title": "Settings", + "description": "Inspect and edit Multiplatform Settings storages.", + "fallback": "disable module on unsupported targets or use multiplatform-settings-stub" + }, + "file_explorer": { + "title": "File Explorer", + "description": "Browse and inspect app files.", + "fallback": "disable module on unsupported targets or use file-explorer-stub" + }, + "layout": { + "title": "Layout (Beta)", + "description": "Inspect UI hierarchy and node properties.", + "fallback": "skip Layout on unsupported platforms (for example WasmJS)" + }, + "overlay": { + "title": "Overlay", + "description": "Floating live metrics panel over app UI.", + "fallback": "disable overlay on unsupported targets or use overlay-stub" + }, + "firebase_cloud_messaging": { + "title": "Firebase Cloud Messaging", + "description": "Inspect push notifications and token data.", + "fallback": "keep module only on Android/iOS; use stub/no-op on JVM/Wasm" + }, + "firebase_analytics": { + "title": "Firebase Analytics", + "description": "Inspect analytics events, user id and properties.", + "fallback": "keep module only on Android/iOS; use stub/no-op on JVM/Wasm" + }, + "runner": { + "title": "Runner", + "description": "Run debug actions and inspect rendered results.", + "fallback": "disable module on unsupported targets or use runner-stub" + } + } +} diff --git a/content/wizard/i18n/es.json b/content/wizard/i18n/es.json new file mode 100644 index 00000000..20fcbcf6 --- /dev/null +++ b/content/wizard/i18n/es.json @@ -0,0 +1,274 @@ +{ + "app": { + "pageTitle": "Asistente de Integración de Kick", + "wizardTitle": "Asistente de integración" + }, + "language": { + "label": "Idioma", + "en": "Inglés", + "ru": "Ruso", + "es": "Español", + "pt": "Portugués", + "zh-Hans": "Chino simplificado", + "ja": "Japonés" + }, + "nav": { + "back": "Atrás", + "next": "Siguiente", + "nextModule": "Siguiente", + "done": "Listo", + "reset": "Restablecer", + "resetConfirm": "¿Restablecer el estado del asistente y borrar las selecciones guardadas?" + }, + "progress": { + "step": "Paso {current} de {total}" + }, + "summary": { + "platforms": "Plataformas", + "modules": "Módulos", + "version": "Versión de Kick", + "count": "{count} seleccionados" + }, + "step": { + "welcome": { + "title": "Genera la integración de Kick en minutos", + "subtitle": "Elige idioma, plataformas y módulos, y obtén snippets de Gradle + Kotlin listos para pegar.", + "checklistTitle": "Flujo del asistente", + "checklistSubtitle": "La página de salida combina todas las decisiones en un solo borrador de integración.", + "itemLanguage": "Selecciona el idioma de la interfaz", + "itemPlatforms": "Selecciona las plataformas de la app", + "itemModules": "Selecciona módulos y configura cada módulo seleccionado", + "itemOutput": "Copia Gradle, Common Kotlin y Platform glue opcional" + }, + "platforms": { + "title": "Seleccionar plataformas", + "subtitle": "Elige todas las plataformas presentes actualmente en tu app." + }, + "modules": { + "title": "Seleccionar módulos", + "subtitle": "Elige módulos para incluir en la inicialización de Kick.", + "selectedCount": "Módulos seleccionados: {count}" + }, + "moduleConfig": { + "title": "Configurar: {module}" + }, + "output": { + "title": "Salida de integración", + "subtitle": "Copia los snippets y pégalos en tu proyecto." + } + }, + "platform": { + "android": "Android", + "ios": "iOS", + "jvm": "JVM (Desktop)", + "wasm": "WasmJS", + "androidDescription": "Target de aplicación Android", + "iosDescription": "Target de aplicación iOS", + "jvmDescription": "Target JVM de escritorio", + "wasmDescription": "Target WebAssembly JS" + }, + "validation": { + "platformRequired": "Selecciona al menos una plataforma antes de continuar.", + "moduleRequired": "Selecciona al menos un módulo antes de continuar." + }, + "modules": { + "unsupportedWarning": "No compatible con: {platforms}", + "unsupportedFallback": "Fallback sugerido: {fallback}" + }, + "badge": { + "highConfiguration": "Configuración alta" + }, + "config": { + "advanced": "Avanzado", + "noQuestions": "No hay preguntas adicionales para este módulo en el flujo por defecto.", + "databaseCount": "Cantidad de bases de datos", + "databaseNameLabel": "Nombre visible #{index}", + "displayName": "Nombre visible", + "storageItem": "Storage #{index}", + "storageDefaultName": "Storage", + "logging": { + "integrateNapier": "Integrar con Napier", + "labelExtractorBracket": "Usar BracketLabelExtractor", + "labelExtractorCustom": "Usar stub de extractor de etiquetas personalizado" + }, + "ktor3": { + "showBodies": "Mostrar cuerpos de request/response", + "redactionKeys": "Claves de redacción (coma o nuevas líneas)" + }, + "overlay": { + "enablePerformanceProvider": "Habilitar provider de rendimiento", + "customProviders": "Providers personalizados (coma o nuevas líneas)" + }, + "runner": { + "generateSampleCalls": "Generar llamadas de ejemplo" + }, + "controlPanel": { + "item": "Elemento #{index}", + "name": "Nombre", + "typeLabel": "Tipo", + "category": "Categoría (opcional)", + "editorLabel": "Editor", + "listValues": "Valores de lista (separados por coma)", + "type": { + "bool": "Bool", + "int": "Int", + "string": "String", + "list": "Lista", + "button": "Botón" + }, + "editor": { + "none": "Ninguno", + "input_number": "Entrada numérica", + "input_string": "Entrada de texto", + "list": "Editor de lista" + } + } + }, + "actions": { + "copy": "Copiar", + "remove": "Eliminar", + "skipModule": "No agregar módulo", + "addStorage": "Agregar storage", + "addItem": "Agregar elemento", + "addExampleItems": "Agregar elementos de ejemplo" + }, + "output": { + "steps": { + "gradle": "Agrega el plugin de Kick en tu shared build.gradle.kts:", + "file": "Crea el archivo {path} con este contenido:", + "ktorExampleTitle": "Ejemplo de integración Ktor3", + "ktorExampleDescription": "Agrega esto al crear tu HttpClient.", + "fcmExampleTitle": "Ejemplo de Firebase Cloud Messaging", + "fcmExampleDescription": "Reenvía payloads push a Kick en callbacks de plataforma.", + "analyticsExampleTitle": "Ejemplo de Firebase Analytics", + "analyticsExampleDescription": "Llama a los métodos de analytics de Kick en el mismo wrapper donde llamas al SDK de Firebase.", + "controlPanelExampleTitle": "Ejemplo de uso de Control Panel", + "controlPanelExampleDescription": "Lee valores de Control Panel y ejecuta lógica de negocio con ellos.", + "overlayExampleTitle": "Ejemplo de actualizaciones de Overlay", + "overlayExampleDescription": "Envía valores en vivo a Overlay desde cualquier parte de tu app.", + "runnerExampleTitle": "Ejemplo de llamada Runner", + "runnerExampleDescription": "Registra una acción de debug y devuelve JSON para inspección." + }, + "gradle": { + "title": "Gradle", + "description": "Configuración del plugin Kick con módulos seleccionados y overrides." + }, + "common": { + "title": "Common Kotlin", + "description": "Crea shared/src/commonMain/kotlin/KickBootstrap.kt y pega este código." + }, + "platformGlue": { + "title": "Platform glue", + "description": "Sigue esta checklist y agrega los archivos de plataforma abajo cuando se generen.", + "pathLabel": "Coloca este archivo en:", + "noFiles": "No se requieren archivos platform glue para la selección actual." + } + }, + "glue": { + "init": { + "android": "Llama a KickBootstrap.init(...) durante el inicio de la app (Application.onCreate o equivalente).", + "ios": "Llama a KickBootstrap.init(...) desde la ruta de arranque compartida usada por el ciclo de vida de iOS.", + "jvm": "Llama a KickBootstrap.init(...) antes de mostrar la ventana de escritorio.", + "wasm": "Llama a KickBootstrap.init(...) al principio del arranque de tu app Wasm." + }, + "hook": { + "firebaseCloudMessagingAndroid": "En FirebaseMessagingService.onMessageReceived(...) llama a Kick.firebaseCloudMessaging.handleFcm(message).", + "firebaseCloudMessagingIos": "Cuando se reciba una notificación APNS, llama a uno de los handlers Kick.firebaseCloudMessaging.handleApns... desde el bridge de Kotlin compartido.", + "firebaseAnalytics": "Duplica las llamadas del wrapper de analytics en Kick.firebaseAnalytics.logEvent / setUserId / setUserProperty." + }, + "unsupportedModule": "{module}: no compatible en esta plataforma.", + "analytics": { + "title": "Configuración rápida de Firebase Analytics", + "subtitle": "Sigue estos pasos en orden. Hazlo tal cual y la integración será consistente.", + "step1": "Crea un wrapper compartido: shared/src/commonMain/kotlin/AnalyticsReporter.kt.", + "step2": "En logEvent(...), llama primero al SDK de Firebase y luego a Kick.firebaseAnalytics.logEvent(name, params).", + "step3": "En setUserId(...), llama primero al SDK de Firebase y luego a Kick.firebaseAnalytics.setUserId(id).", + "step4": "En setUserProperty(...), llama primero al SDK de Firebase y luego a Kick.firebaseAnalytics.setUserProperty(name, value).", + "step5": "Reemplaza las llamadas directas a Firebase por este wrapper en toda la app." + }, + "guide": { + "title": "Qué conectar en el código de tu app", + "subtitle": "Sigue estos pasos. Solo se muestran los puntos relevantes para los módulos seleccionados.", + "logging": "Para enviar logs al módulo Logging, llama a Kick.log(level, message) en los lugares que quieras registrar.", + "loggingWithNapier": "Para enviar logs al módulo Logging, llama a Kick.log(level, message) donde haga falta, o redirige todos los logs de Napier con installNapierBridge() desde KickBootstrap.", + "ktor3": "Para registrar actividad de red de Ktor3, agrega install(KickKtor3Plugin) al crear HttpClient.", + "firebaseCloudMessagingBoth": "Para Firebase Cloud Messaging: en Android llama a Kick.firebaseCloudMessaging.handleFcm(message) dentro de FirebaseMessagingService.onMessageReceived(...). En iOS llama a KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo) en AppDelegate cuando recibas payload APNS.", + "firebaseCloudMessagingAndroid": "Para Firebase Cloud Messaging en Android: llama a Kick.firebaseCloudMessaging.handleFcm(message) dentro de FirebaseMessagingService.onMessageReceived(...).", + "firebaseCloudMessagingIos": "Para Firebase Cloud Messaging en iOS: llama a KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo) en AppDelegate cuando recibas payload APNS.", + "firebaseCloudMessagingGeneric": "Para Firebase Cloud Messaging, reenvía payloads push a los handlers de Kick en el ciclo de vida de la plataforma.", + "firebaseAnalytics": "Para Firebase Analytics, agrega Kick.firebaseAnalytics.logEvent(...), Kick.firebaseAnalytics.setUserId(...), y Kick.firebaseAnalytics.setUserProperty(...) en los mismos puntos de tu wrapper de analytics.", + "controlPanel": "Lee valores de ControlPanel en tu código con Kick.controlPanel.getBoolean(...), Kick.controlPanel.getString(...), etc. Si agregaste botones, escucha clics con Kick.controlPanel.events.collect { event -> ... }.", + "overlay": "Muestra valores runtime en la ventana flotante Overlay con Kick.overlay.set(\"key\", value).", + "runner": "Registra acciones de debug útiles donde lo necesites con Kick.runner.addCall(...)." + } + }, + "toast": { + "copied": "Copiado", + "copyFailed": "Error al copiar" + }, + "errors": { + "loadFailed": "No se pudieron cargar los recursos del asistente." + }, + "module": { + "sqldelight": { + "title": "SQLDelight", + "description": "Ver y editar bases de datos SQLDelight.", + "fallback": "mantén SQLDelight solo en targets compatibles o usa stub/no-op en builds no compatibles" + }, + "room": { + "title": "Room", + "description": "Inspeccionar tablas y valores de Room.", + "fallback": "usa SQLDelight en targets Web o proporciona fallback no-op para Room" + }, + "logging": { + "title": "Logging", + "description": "Visor de logs en vivo con soporte de filtrado.", + "fallback": "desactiva el módulo en targets no compatibles o usa stub/no-op" + }, + "ktor3": { + "title": "Ktor3", + "description": "Monitor de red para requests y responses de Ktor.", + "fallback": "desactiva el módulo en targets no compatibles o usa stub/no-op" + }, + "control_panel": { + "title": "Control Panel", + "description": "Toggles runtime de funciones y valores debug editables.", + "fallback": "desactiva el módulo en targets no compatibles o usa control-panel-stub" + }, + "multiplatform_settings": { + "title": "Settings", + "description": "Inspeccionar y editar storages de Multiplatform Settings.", + "fallback": "desactiva el módulo en targets no compatibles o usa multiplatform-settings-stub" + }, + "file_explorer": { + "title": "File Explorer", + "description": "Navegar e inspeccionar archivos de la app.", + "fallback": "desactiva el módulo en targets no compatibles o usa file-explorer-stub" + }, + "layout": { + "title": "Layout (Beta)", + "description": "Inspeccionar jerarquía de UI y propiedades de nodos.", + "fallback": "omite Layout en plataformas no compatibles (por ejemplo WasmJS)" + }, + "overlay": { + "title": "Overlay", + "description": "Panel flotante de métricas en vivo sobre la UI de la app.", + "fallback": "desactiva overlay en targets no compatibles o usa overlay-stub" + }, + "firebase_cloud_messaging": { + "title": "Firebase Cloud Messaging", + "description": "Inspeccionar notificaciones push y datos de token.", + "fallback": "mantén el módulo solo en Android/iOS; usa stub/no-op en JVM/Wasm" + }, + "firebase_analytics": { + "title": "Firebase Analytics", + "description": "Inspeccionar eventos de analytics, user id y propiedades.", + "fallback": "mantén el módulo solo en Android/iOS; usa stub/no-op en JVM/Wasm" + }, + "runner": { + "title": "Runner", + "description": "Ejecutar acciones de debug e inspeccionar resultados renderizados.", + "fallback": "desactiva el módulo en targets no compatibles o usa runner-stub" + } + } +} diff --git a/content/wizard/i18n/ja.json b/content/wizard/i18n/ja.json new file mode 100644 index 00000000..e58f75b9 --- /dev/null +++ b/content/wizard/i18n/ja.json @@ -0,0 +1,274 @@ +{ + "app": { + "pageTitle": "Kick 統合ウィザード", + "wizardTitle": "統合ウィザード" + }, + "language": { + "label": "言語", + "en": "英語", + "ru": "ロシア語", + "es": "スペイン語", + "pt": "ポルトガル語", + "zh-Hans": "中国語(簡体字)", + "ja": "日本語" + }, + "nav": { + "back": "戻る", + "next": "次へ", + "nextModule": "次へ", + "done": "完了", + "reset": "リセット", + "resetConfirm": "ウィザードの状態をリセットし、保存済みの選択を削除しますか?" + }, + "progress": { + "step": "ステップ {current} / {total}" + }, + "summary": { + "platforms": "プラットフォーム", + "modules": "モジュール", + "version": "Kick バージョン", + "count": "{count} 件選択" + }, + "step": { + "welcome": { + "title": "数分で Kick 統合を生成", + "subtitle": "言語・プラットフォーム・モジュールを選択し、貼り付け可能な Gradle + Kotlin スニペットを生成します。", + "checklistTitle": "ウィザードの流れ", + "checklistSubtitle": "出力ページですべての選択を 1 つの統合ドラフトにまとめます。", + "itemLanguage": "UI 言語を選択", + "itemPlatforms": "アプリのプラットフォームを選択", + "itemModules": "モジュールを選択し、選んだ各モジュールを設定", + "itemOutput": "Gradle、Common Kotlin、必要に応じて Platform glue をコピー" + }, + "platforms": { + "title": "プラットフォームを選択", + "subtitle": "現在アプリで使用しているすべてのプラットフォームを選択してください。" + }, + "modules": { + "title": "モジュールを選択", + "subtitle": "Kick 初期化に含めるモジュールを選びます。", + "selectedCount": "選択したモジュール: {count}" + }, + "moduleConfig": { + "title": "設定: {module}" + }, + "output": { + "title": "統合出力", + "subtitle": "スニペットをコピーしてプロジェクトに貼り付けてください。" + } + }, + "platform": { + "android": "Android", + "ios": "iOS", + "jvm": "JVM(Desktop)", + "wasm": "WasmJS", + "androidDescription": "Android アプリターゲット", + "iosDescription": "iOS アプリターゲット", + "jvmDescription": "デスクトップ JVM ターゲット", + "wasmDescription": "WebAssembly JS ターゲット" + }, + "validation": { + "platformRequired": "先に進むには少なくとも 1 つのプラットフォームを選択してください。", + "moduleRequired": "先に進むには少なくとも 1 つのモジュールを選択してください。" + }, + "modules": { + "unsupportedWarning": "未対応: {platforms}", + "unsupportedFallback": "推奨フォールバック: {fallback}" + }, + "badge": { + "highConfiguration": "高い設定難易度" + }, + "config": { + "advanced": "詳細設定", + "noQuestions": "このモジュールはデフォルトフローでは追加の質問がありません。", + "databaseCount": "データベース数", + "databaseNameLabel": "表示名 #{index}", + "displayName": "表示名", + "storageItem": "Storage #{index}", + "storageDefaultName": "Storage", + "logging": { + "integrateNapier": "Napier と連携", + "labelExtractorBracket": "BracketLabelExtractor を使用", + "labelExtractorCustom": "カスタム label extractor のスタブを使用" + }, + "ktor3": { + "showBodies": "リクエスト/レスポンス body を表示", + "redactionKeys": "マスキングキー(カンマまたは改行区切り)" + }, + "overlay": { + "enablePerformanceProvider": "パフォーマンス provider を有効化", + "customProviders": "カスタム provider(カンマまたは改行区切り)" + }, + "runner": { + "generateSampleCalls": "サンプル呼び出しを生成" + }, + "controlPanel": { + "item": "項目 #{index}", + "name": "名前", + "typeLabel": "タイプ", + "category": "カテゴリ(任意)", + "editorLabel": "エディタ", + "listValues": "リスト値(カンマ区切り)", + "type": { + "bool": "Bool", + "int": "Int", + "string": "String", + "list": "List", + "button": "Button" + }, + "editor": { + "none": "なし", + "input_number": "数値入力", + "input_string": "文字列入力", + "list": "リストエディタ" + } + } + }, + "actions": { + "copy": "コピー", + "remove": "削除", + "skipModule": "モジュールを追加しない", + "addStorage": "Storage を追加", + "addItem": "項目を追加", + "addExampleItems": "サンプル項目を追加" + }, + "output": { + "steps": { + "gradle": "shared build.gradle.kts に Kick プラグインを追加:", + "file": "{path} を作成し、以下の内容を貼り付け:", + "ktorExampleTitle": "Ktor3 統合例", + "ktorExampleDescription": "HttpClient 作成時にこれを追加してください。", + "fcmExampleTitle": "Firebase Cloud Messaging 例", + "fcmExampleDescription": "プラットフォームのコールバックで push payload を Kick に渡します。", + "analyticsExampleTitle": "Firebase Analytics 例", + "analyticsExampleDescription": "Firebase SDK を呼ぶ同じ wrapper 内で Kick analytics メソッドも呼びます。", + "controlPanelExampleTitle": "Control Panel 利用例", + "controlPanelExampleDescription": "Control Panel の値を読み取り、業務ロジックに利用します。", + "overlayExampleTitle": "Overlay 更新例", + "overlayExampleDescription": "アプリ内の任意箇所から Overlay にライブ値を送ります。", + "runnerExampleTitle": "Runner 呼び出し例", + "runnerExampleDescription": "デバッグアクションを登録し、確認用 JSON を返します。" + }, + "gradle": { + "title": "Gradle", + "description": "選択したモジュールと override を含む Kick プラグイン設定。" + }, + "common": { + "title": "Common Kotlin", + "description": "shared/src/commonMain/kotlin/KickBootstrap.kt を作成してこのコードを貼り付けます。" + }, + "platformGlue": { + "title": "Platform glue", + "description": "このチェックリストに従い、生成された場合のみ下記プラットフォームファイルを追加してください。", + "pathLabel": "このパスに配置:", + "noFiles": "現在の選択では追加の platform glue ファイルは不要です。" + } + }, + "glue": { + "init": { + "android": "アプリ起動時(Application.onCreate など)に KickBootstrap.init(...) を呼び出します。", + "ios": "iOS のライフサイクルで使う共有スタートアップ経路から KickBootstrap.init(...) を呼び出します。", + "jvm": "デスクトップのウィンドウ表示前に KickBootstrap.init(...) を呼び出します。", + "wasm": "Wasm アプリの起動初期で KickBootstrap.init(...) を呼び出します。" + }, + "hook": { + "firebaseCloudMessagingAndroid": "FirebaseMessagingService.onMessageReceived(...) で Kick.firebaseCloudMessaging.handleFcm(message) を呼びます。", + "firebaseCloudMessagingIos": "APNS 通知受信時に、共有 Kotlin API bridge から Kick.firebaseCloudMessaging.handleApns... を呼びます。", + "firebaseAnalytics": "analytics wrapper の呼び出しを Kick.firebaseAnalytics.logEvent / setUserId / setUserProperty にもミラーします。" + }, + "unsupportedModule": "{module}: このプラットフォームでは未対応です。", + "analytics": { + "title": "Firebase Analytics クイックセットアップ", + "subtitle": "この順番で実行してください。手順通りに行えば統合を一貫して維持できます。", + "step1": "共有 wrapper ファイルを 1 つ作成: shared/src/commonMain/kotlin/AnalyticsReporter.kt。", + "step2": "logEvent(...) で先に Firebase SDK、次に Kick.firebaseAnalytics.logEvent(name, params) を呼びます。", + "step3": "setUserId(...) で先に Firebase SDK、次に Kick.firebaseAnalytics.setUserId(id) を呼びます。", + "step4": "setUserProperty(...) で先に Firebase SDK、次に Kick.firebaseAnalytics.setUserProperty(name, value) を呼びます。", + "step5": "アプリ全体で Firebase 直接呼び出しをこの wrapper に置き換えます。" + }, + "guide": { + "title": "アプリコードで接続する内容", + "subtitle": "以下の手順に従ってください。選択したモジュールに関係する項目のみ表示されます。", + "logging": "Logging モジュールにログを送るには、必要な箇所で Kick.log(level, message) を呼び出します。", + "loggingWithNapier": "Logging モジュールにログを送るには、必要箇所で Kick.log(level, message) を呼ぶか、KickBootstrap の installNapierBridge() で Napier ログをすべて転送します。", + "ktor3": "Ktor3 のネットワーク活動を記録するには、HttpClient 作成時に install(KickKtor3Plugin) を追加します。", + "firebaseCloudMessagingBoth": "Firebase Cloud Messaging: Android では FirebaseMessagingService.onMessageReceived(...) 内で Kick.firebaseCloudMessaging.handleFcm(message) を呼びます。iOS では APNS payload 受信時に AppDelegate で KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo) を呼びます。", + "firebaseCloudMessagingAndroid": "Android の Firebase Cloud Messaging: FirebaseMessagingService.onMessageReceived(...) 内で Kick.firebaseCloudMessaging.handleFcm(message) を呼びます。", + "firebaseCloudMessagingIos": "iOS の Firebase Cloud Messaging: APNS payload 受信時に AppDelegate で KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo) を呼びます。", + "firebaseCloudMessagingGeneric": "Firebase Cloud Messaging では、プッシュ payload をプラットフォームのライフサイクルコードから Kick ハンドラに転送してください。", + "firebaseAnalytics": "Firebase Analytics では、analytics wrapper の対応箇所で Kick.firebaseAnalytics.logEvent(...)、Kick.firebaseAnalytics.setUserId(...)、Kick.firebaseAnalytics.setUserProperty(...) を呼び出します。", + "controlPanel": "コード中で Kick.controlPanel.getBoolean(...)、Kick.controlPanel.getString(...) などを使って ControlPanel の値を取得します。ボタンを追加した場合は Kick.controlPanel.events.collect { event -> ... } でクリックを監視します。", + "overlay": "Kick.overlay.set(\"key\", value) を使って Overlay のフローティングウィンドウに必要な値を表示します。", + "runner": "必要な箇所で Kick.runner.addCall(...) を使ってデバッグアクションを登録します。" + } + }, + "toast": { + "copied": "コピーしました", + "copyFailed": "コピーに失敗しました" + }, + "errors": { + "loadFailed": "ウィザードリソースの読み込みに失敗しました。" + }, + "module": { + "sqldelight": { + "title": "SQLDelight", + "description": "SQLDelight データベースを表示・編集します。", + "fallback": "SQLDelight は対応ターゲットのみに限定するか、非対応ビルドでは stub/no-op を使用" + }, + "room": { + "title": "Room", + "description": "Room データベースのテーブルと値を確認します。", + "fallback": "Web ターゲットでは SQLDelight を使うか、Room 用に no-op fallback を用意" + }, + "logging": { + "title": "Logging", + "description": "フィルタ対応のライブログビューア。", + "fallback": "非対応ターゲットでは無効化するか stub/no-op を使用" + }, + "ktor3": { + "title": "Ktor3", + "description": "Ktor リクエスト/レスポンス用ネットワークモニタ。", + "fallback": "非対応ターゲットでは無効化するか stub/no-op を使用" + }, + "control_panel": { + "title": "Control Panel", + "description": "ランタイム機能トグルと編集可能なデバッグ値。", + "fallback": "非対応ターゲットでは無効化するか control-panel-stub を使用" + }, + "multiplatform_settings": { + "title": "Settings", + "description": "Multiplatform Settings のストレージを表示・編集します。", + "fallback": "非対応ターゲットでは無効化するか multiplatform-settings-stub を使用" + }, + "file_explorer": { + "title": "File Explorer", + "description": "アプリファイルを参照・確認します。", + "fallback": "非対応ターゲットでは無効化するか file-explorer-stub を使用" + }, + "layout": { + "title": "Layout (Beta)", + "description": "UI 階層とノードプロパティを検査します。", + "fallback": "非対応プラットフォーム(例: WasmJS)では Layout をスキップ" + }, + "overlay": { + "title": "Overlay", + "description": "アプリ UI 上に表示するフローティングライブメトリクスパネル。", + "fallback": "非対応ターゲットでは overlay を無効化するか overlay-stub を使用" + }, + "firebase_cloud_messaging": { + "title": "Firebase Cloud Messaging", + "description": "プッシュ通知とトークン情報を確認します。", + "fallback": "このモジュールは Android/iOS のみで使用。JVM/Wasm では stub/no-op を使用" + }, + "firebase_analytics": { + "title": "Firebase Analytics", + "description": "analytics イベント、user id、プロパティを確認します。", + "fallback": "このモジュールは Android/iOS のみで使用。JVM/Wasm では stub/no-op を使用" + }, + "runner": { + "title": "Runner", + "description": "デバッグアクションを実行し、レンダリング結果を確認します。", + "fallback": "非対応ターゲットでは無効化するか runner-stub を使用" + } + } +} diff --git a/content/wizard/i18n/pt.json b/content/wizard/i18n/pt.json new file mode 100644 index 00000000..4922f031 --- /dev/null +++ b/content/wizard/i18n/pt.json @@ -0,0 +1,274 @@ +{ + "app": { + "pageTitle": "Assistente de Integração do Kick", + "wizardTitle": "Assistente de integração" + }, + "language": { + "label": "Idioma", + "en": "Inglês", + "ru": "Russo", + "es": "Espanhol", + "pt": "Português", + "zh-Hans": "Chinês simplificado", + "ja": "Japonês" + }, + "nav": { + "back": "Voltar", + "next": "Próximo", + "nextModule": "Próximo", + "done": "Concluir", + "reset": "Reiniciar", + "resetConfirm": "Reiniciar o estado do assistente e limpar as seleções salvas?" + }, + "progress": { + "step": "Etapa {current} de {total}" + }, + "summary": { + "platforms": "Plataformas", + "modules": "Módulos", + "version": "Versão do Kick", + "count": "{count} selecionados" + }, + "step": { + "welcome": { + "title": "Gere a integração do Kick em minutos", + "subtitle": "Escolha idioma, plataformas e módulos, e gere snippets Gradle + Kotlin prontos para colar.", + "checklistTitle": "Fluxo do assistente", + "checklistSubtitle": "A página de saída reúne todas as escolhas em um único rascunho de integração.", + "itemLanguage": "Selecione o idioma da interface", + "itemPlatforms": "Selecione as plataformas do app", + "itemModules": "Selecione módulos e configure cada módulo selecionado", + "itemOutput": "Copie Gradle, Common Kotlin e Platform glue opcional" + }, + "platforms": { + "title": "Selecionar plataformas", + "subtitle": "Escolha todas as plataformas presentes no seu app." + }, + "modules": { + "title": "Selecionar módulos", + "subtitle": "Escolha os módulos para incluir na inicialização do Kick.", + "selectedCount": "Módulos selecionados: {count}" + }, + "moduleConfig": { + "title": "Configurar: {module}" + }, + "output": { + "title": "Saída da integração", + "subtitle": "Copie os snippets e cole no seu projeto." + } + }, + "platform": { + "android": "Android", + "ios": "iOS", + "jvm": "JVM (Desktop)", + "wasm": "WasmJS", + "androidDescription": "Target de aplicativo Android", + "iosDescription": "Target de aplicativo iOS", + "jvmDescription": "Target JVM de desktop", + "wasmDescription": "Target WebAssembly JS" + }, + "validation": { + "platformRequired": "Selecione pelo menos uma plataforma antes de continuar.", + "moduleRequired": "Selecione pelo menos um módulo antes de continuar." + }, + "modules": { + "unsupportedWarning": "Não suportado em: {platforms}", + "unsupportedFallback": "Fallback sugerido: {fallback}" + }, + "badge": { + "highConfiguration": "Configuração alta" + }, + "config": { + "advanced": "Avançado", + "noQuestions": "Não há perguntas adicionais para este módulo no fluxo padrão.", + "databaseCount": "Quantidade de bancos de dados", + "databaseNameLabel": "Nome de exibição #{index}", + "displayName": "Nome de exibição", + "storageItem": "Storage #{index}", + "storageDefaultName": "Storage", + "logging": { + "integrateNapier": "Integrar com Napier", + "labelExtractorBracket": "Usar BracketLabelExtractor", + "labelExtractorCustom": "Usar stub de extractor de labels customizado" + }, + "ktor3": { + "showBodies": "Mostrar corpos de request/response", + "redactionKeys": "Chaves de redação (vírgulas ou novas linhas)" + }, + "overlay": { + "enablePerformanceProvider": "Ativar provider de performance", + "customProviders": "Providers personalizados (vírgulas ou novas linhas)" + }, + "runner": { + "generateSampleCalls": "Gerar chamadas de exemplo" + }, + "controlPanel": { + "item": "Item #{index}", + "name": "Nome", + "typeLabel": "Tipo", + "category": "Categoria (opcional)", + "editorLabel": "Editor", + "listValues": "Valores da lista (separados por vírgula)", + "type": { + "bool": "Bool", + "int": "Int", + "string": "String", + "list": "Lista", + "button": "Botão" + }, + "editor": { + "none": "Nenhum", + "input_number": "Entrada numérica", + "input_string": "Entrada de texto", + "list": "Editor de lista" + } + } + }, + "actions": { + "copy": "Copiar", + "remove": "Remover", + "skipModule": "Não adicionar módulo", + "addStorage": "Adicionar storage", + "addItem": "Adicionar item", + "addExampleItems": "Adicionar itens de exemplo" + }, + "output": { + "steps": { + "gradle": "Adicione o plugin do Kick no seu shared build.gradle.kts:", + "file": "Crie o arquivo {path} com este conteúdo:", + "ktorExampleTitle": "Exemplo de integração Ktor3", + "ktorExampleDescription": "Adicione isso ao criar o seu HttpClient.", + "fcmExampleTitle": "Exemplo de Firebase Cloud Messaging", + "fcmExampleDescription": "Encaminhe payloads push para o Kick nos callbacks de plataforma.", + "analyticsExampleTitle": "Exemplo de Firebase Analytics", + "analyticsExampleDescription": "Chame os métodos de analytics do Kick no mesmo wrapper em que você chama o SDK do Firebase.", + "controlPanelExampleTitle": "Exemplo de uso do Control Panel", + "controlPanelExampleDescription": "Leia valores do Control Panel e execute lógica de negócio com eles.", + "overlayExampleTitle": "Exemplo de atualização do Overlay", + "overlayExampleDescription": "Envie valores em tempo real para o Overlay de qualquer lugar do app.", + "runnerExampleTitle": "Exemplo de chamada do Runner", + "runnerExampleDescription": "Registre uma ação de debug e retorne JSON para inspeção." + }, + "gradle": { + "title": "Gradle", + "description": "Configuração do plugin Kick com módulos selecionados e overrides." + }, + "common": { + "title": "Common Kotlin", + "description": "Crie shared/src/commonMain/kotlin/KickBootstrap.kt e cole este código." + }, + "platformGlue": { + "title": "Platform glue", + "description": "Siga este checklist e adicione os arquivos de plataforma abaixo quando forem gerados.", + "pathLabel": "Coloque este arquivo em:", + "noFiles": "Nenhum arquivo de platform glue é necessário para a seleção atual." + } + }, + "glue": { + "init": { + "android": "Chame KickBootstrap.init(...) durante a inicialização do app (Application.onCreate ou equivalente).", + "ios": "Chame KickBootstrap.init(...) no caminho de startup compartilhado usado pelo ciclo de vida do app iOS.", + "jvm": "Chame KickBootstrap.init(...) antes de exibir a janela desktop.", + "wasm": "Chame KickBootstrap.init(...) no início do startup do seu app Wasm." + }, + "hook": { + "firebaseCloudMessagingAndroid": "Em FirebaseMessagingService.onMessageReceived(...) chame Kick.firebaseCloudMessaging.handleFcm(message).", + "firebaseCloudMessagingIos": "Quando uma notificação APNS for recebida, chame um dos handlers Kick.firebaseCloudMessaging.handleApns... a partir da bridge Kotlin compartilhada.", + "firebaseAnalytics": "Espelhe as chamadas do wrapper de analytics em Kick.firebaseAnalytics.logEvent / setUserId / setUserProperty." + }, + "unsupportedModule": "{module}: não suportado nesta plataforma.", + "analytics": { + "title": "Configuração rápida do Firebase Analytics", + "subtitle": "Siga estes passos na ordem. Faça exatamente assim e a integração ficará consistente.", + "step1": "Crie um wrapper compartilhado: shared/src/commonMain/kotlin/AnalyticsReporter.kt.", + "step2": "Em logEvent(...), chame primeiro o SDK do Firebase e depois Kick.firebaseAnalytics.logEvent(name, params).", + "step3": "Em setUserId(...), chame primeiro o SDK do Firebase e depois Kick.firebaseAnalytics.setUserId(id).", + "step4": "Em setUserProperty(...), chame primeiro o SDK do Firebase e depois Kick.firebaseAnalytics.setUserProperty(name, value).", + "step5": "Substitua chamadas diretas ao Firebase por este wrapper em todo o app." + }, + "guide": { + "title": "O que conectar no código do app", + "subtitle": "Siga estes passos. Apenas itens relevantes para os módulos selecionados são exibidos.", + "logging": "Para enviar logs ao módulo Logging, chame Kick.log(level, message) nos pontos que você deseja logar.", + "loggingWithNapier": "Para enviar logs ao módulo Logging, chame Kick.log(level, message) onde necessário ou redirecione todos os logs do Napier com installNapierBridge() a partir do KickBootstrap.", + "ktor3": "Para logar atividade de rede do Ktor3, adicione install(KickKtor3Plugin) ao criar o HttpClient.", + "firebaseCloudMessagingBoth": "Para Firebase Cloud Messaging: no Android, chame Kick.firebaseCloudMessaging.handleFcm(message) dentro de FirebaseMessagingService.onMessageReceived(...). No iOS, chame KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo) no AppDelegate quando um payload APNS for recebido.", + "firebaseCloudMessagingAndroid": "Para Firebase Cloud Messaging no Android: chame Kick.firebaseCloudMessaging.handleFcm(message) dentro de FirebaseMessagingService.onMessageReceived(...).", + "firebaseCloudMessagingIos": "Para Firebase Cloud Messaging no iOS: chame KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo) no AppDelegate quando um payload APNS for recebido.", + "firebaseCloudMessagingGeneric": "Para Firebase Cloud Messaging, encaminhe payloads push para os handlers do Kick no código de ciclo de vida da plataforma.", + "firebaseAnalytics": "Para Firebase Analytics, adicione Kick.firebaseAnalytics.logEvent(...), Kick.firebaseAnalytics.setUserId(...), e Kick.firebaseAnalytics.setUserProperty(...) nos mesmos pontos do seu wrapper de analytics.", + "controlPanel": "Leia valores do ControlPanel no seu código com Kick.controlPanel.getBoolean(...), Kick.controlPanel.getString(...), etc. Se você adicionou botões, escute cliques com Kick.controlPanel.events.collect { event -> ... }.", + "overlay": "Mostre valores runtime na janela flutuante do Overlay com Kick.overlay.set(\"key\", value).", + "runner": "Registre ações de debug úteis nos locais que precisar com Kick.runner.addCall(...)." + } + }, + "toast": { + "copied": "Copiado", + "copyFailed": "Falha ao copiar" + }, + "errors": { + "loadFailed": "Falha ao carregar os recursos do assistente." + }, + "module": { + "sqldelight": { + "title": "SQLDelight", + "description": "Visualizar e editar bancos SQLDelight.", + "fallback": "mantenha SQLDelight apenas em targets suportados ou use stub/no-op em builds não suportados" + }, + "room": { + "title": "Room", + "description": "Inspecionar tabelas e valores do Room.", + "fallback": "use SQLDelight em targets Web ou forneça fallback no-op para Room" + }, + "logging": { + "title": "Logging", + "description": "Visualizador de logs em tempo real com suporte a filtros.", + "fallback": "desative o módulo em targets não suportados ou use stub/no-op" + }, + "ktor3": { + "title": "Ktor3", + "description": "Monitor de rede para requests e responses do Ktor.", + "fallback": "desative o módulo em targets não suportados ou use stub/no-op" + }, + "control_panel": { + "title": "Control Panel", + "description": "Toggles runtime de recursos e valores debug editáveis.", + "fallback": "desative o módulo em targets não suportados ou use control-panel-stub" + }, + "multiplatform_settings": { + "title": "Settings", + "description": "Inspecionar e editar storages do Multiplatform Settings.", + "fallback": "desative o módulo em targets não suportados ou use multiplatform-settings-stub" + }, + "file_explorer": { + "title": "File Explorer", + "description": "Navegar e inspecionar arquivos do app.", + "fallback": "desative o módulo em targets não suportados ou use file-explorer-stub" + }, + "layout": { + "title": "Layout (Beta)", + "description": "Inspecionar a hierarquia de UI e propriedades dos nós.", + "fallback": "ignore Layout em plataformas não suportadas (por exemplo WasmJS)" + }, + "overlay": { + "title": "Overlay", + "description": "Painel flutuante de métricas em tempo real sobre a UI do app.", + "fallback": "desative o overlay em targets não suportados ou use overlay-stub" + }, + "firebase_cloud_messaging": { + "title": "Firebase Cloud Messaging", + "description": "Inspecionar notificações push e dados de token.", + "fallback": "mantenha o módulo apenas em Android/iOS; use stub/no-op em JVM/Wasm" + }, + "firebase_analytics": { + "title": "Firebase Analytics", + "description": "Inspecionar eventos de analytics, user id e propriedades.", + "fallback": "mantenha o módulo apenas em Android/iOS; use stub/no-op em JVM/Wasm" + }, + "runner": { + "title": "Runner", + "description": "Executar ações de debug e inspecionar resultados renderizados.", + "fallback": "desative o módulo em targets não suportados ou use runner-stub" + } + } +} diff --git a/content/wizard/i18n/ru.json b/content/wizard/i18n/ru.json new file mode 100644 index 00000000..9c420487 --- /dev/null +++ b/content/wizard/i18n/ru.json @@ -0,0 +1,274 @@ +{ + "app": { + "pageTitle": "Kick Integration Wizard", + "wizardTitle": "Мастер интеграции" + }, + "language": { + "label": "Язык", + "en": "English", + "ru": "Русский", + "es": "Español", + "pt": "Português", + "zh-Hans": "简体中文", + "ja": "日本語" + }, + "nav": { + "back": "Назад", + "next": "Далее", + "nextModule": "Далее", + "done": "Готово", + "reset": "Сброс", + "resetConfirm": "Сбросить мастер и очистить сохранённые выборы?" + }, + "progress": { + "step": "Шаг {current} из {total}" + }, + "summary": { + "platforms": "Платформы", + "modules": "Модули", + "version": "Версия Kick", + "count": "Выбрано: {count}" + }, + "step": { + "welcome": { + "title": "Соберите интеграцию Kick за пару минут", + "subtitle": "Выберите язык, платформы, модули и получите готовые сниппеты Gradle + Kotlin.", + "checklistTitle": "Поток мастера", + "checklistSubtitle": "На выходе будет одна страница со всеми собранными инструкциями.", + "itemLanguage": "Выбрать язык интерфейса", + "itemPlatforms": "Выбрать платформы приложения", + "itemModules": "Выбрать модули и настроить каждый выбранный модуль", + "itemOutput": "Скопировать Gradle, Common Kotlin и при необходимости Platform glue" + }, + "platforms": { + "title": "Выбор платформ", + "subtitle": "Отметьте все платформы, которые есть в вашем приложении." + }, + "modules": { + "title": "Выбор модулей", + "subtitle": "Выберите модули для инициализации Kick.", + "selectedCount": "Выбрано модулей: {count}" + }, + "moduleConfig": { + "title": "Настройка: {module}" + }, + "output": { + "title": "Итоговый Output", + "subtitle": "Скопируйте сниппеты и вставьте их в проект." + } + }, + "platform": { + "android": "Android", + "ios": "iOS", + "jvm": "JVM (Desktop)", + "wasm": "WasmJS", + "androidDescription": "Таргет Android", + "iosDescription": "Таргет iOS", + "jvmDescription": "Десктопный JVM-таргет", + "wasmDescription": "WebAssembly JS-таргет" + }, + "validation": { + "platformRequired": "Перед продолжением выберите хотя бы одну платформу.", + "moduleRequired": "Перед продолжением выберите хотя бы один модуль." + }, + "modules": { + "unsupportedWarning": "Не поддерживается на: {platforms}", + "unsupportedFallback": "Рекомендуемый обход: {fallback}" + }, + "badge": { + "highConfiguration": "High configuration" + }, + "config": { + "advanced": "Advanced", + "noQuestions": "Для этого модуля в базовом потоке нет дополнительных вопросов.", + "databaseCount": "Количество баз данных", + "databaseNameLabel": "Отображаемое имя #{index}", + "displayName": "Отображаемое имя", + "storageItem": "Хранилище #{index}", + "storageDefaultName": "Хранилище", + "logging": { + "integrateNapier": "Интегрировать с Napier", + "labelExtractorBracket": "Использовать BracketLabelExtractor", + "labelExtractorCustom": "Использовать custom label extractor (stub)" + }, + "ktor3": { + "showBodies": "Показывать body request/response", + "redactionKeys": "Ключи редактирования (через запятую или с новой строки)" + }, + "overlay": { + "enablePerformanceProvider": "Включить performance provider", + "customProviders": "Custom providers (через запятую или с новой строки)" + }, + "runner": { + "generateSampleCalls": "Сгенерировать sample calls" + }, + "controlPanel": { + "item": "Элемент #{index}", + "name": "Название", + "typeLabel": "Тип", + "category": "Категория (опционально)", + "editorLabel": "Редактор", + "listValues": "Значения списка (через запятую)", + "type": { + "bool": "Bool", + "int": "Int", + "string": "String", + "list": "List", + "button": "Button" + }, + "editor": { + "none": "Нет", + "input_number": "Числовой input", + "input_string": "Строковый input", + "list": "Редактор списка" + } + } + }, + "actions": { + "copy": "Копировать", + "remove": "Удалить", + "skipModule": "Не добавлять модуль", + "addStorage": "Добавить storage", + "addItem": "Добавить item", + "addExampleItems": "Добавить пример items" + }, + "output": { + "steps": { + "gradle": "Добавьте Kick plugin в shared build.gradle.kts:", + "file": "Создайте файл {path} со следующим содержимым:", + "ktorExampleTitle": "Пример интеграции Ktor3", + "ktorExampleDescription": "Добавьте это при создании HttpClient.", + "fcmExampleTitle": "Пример Firebase Cloud Messaging", + "fcmExampleDescription": "Пробрасывайте push payload в Kick в platform callback-ах.", + "analyticsExampleTitle": "Пример Firebase Analytics", + "analyticsExampleDescription": "Вызывайте методы Kick analytics в том же wrapper, где вызываете Firebase SDK.", + "controlPanelExampleTitle": "Пример использования Control Panel", + "controlPanelExampleDescription": "Получайте значения Control Panel и запускайте бизнес-логику по ним.", + "overlayExampleTitle": "Пример обновления Overlay", + "overlayExampleDescription": "Отправляйте live-значения в Overlay из нужных мест приложения.", + "runnerExampleTitle": "Пример вызова Runner", + "runnerExampleDescription": "Зарегистрируйте debug-действие и верните JSON для просмотра." + }, + "gradle": { + "title": "Gradle", + "description": "Настройка Kick plugin с выбранными модулями и override-параметрами." + }, + "common": { + "title": "Common Kotlin", + "description": "Создайте shared/src/commonMain/kotlin/KickBootstrap.kt и вставьте этот код." + }, + "platformGlue": { + "title": "Platform glue", + "description": "Пройдите чеклист ниже и добавьте platform-файлы только если они сгенерированы.", + "pathLabel": "Положите файл в:", + "noFiles": "Для текущего набора модулей дополнительные platform glue файлы не нужны." + } + }, + "glue": { + "init": { + "android": "Вызовите KickBootstrap.init(...) на старте приложения (Application.onCreate или эквивалент).", + "ios": "Вызовите KickBootstrap.init(...) в общем пути инициализации, который используется жизненным циклом iOS-приложения.", + "jvm": "Вызовите KickBootstrap.init(...) до показа desktop-окна.", + "wasm": "Вызовите KickBootstrap.init(...) в ранней инициализации Wasm-приложения." + }, + "hook": { + "firebaseCloudMessagingAndroid": "В FirebaseMessagingService.onMessageReceived(...) вызывайте Kick.firebaseCloudMessaging.handleFcm(message).", + "firebaseCloudMessagingIos": "При получении APNS вызывать один из обработчиков Kick.firebaseCloudMessaging.handleApns... через bridge к Kotlin API.", + "firebaseAnalytics": "Дублируйте вызовы analytics-wrapper в Kick.firebaseAnalytics.logEvent / setUserId / setUserProperty." + }, + "unsupportedModule": "{module}: модуль не поддерживается на этой платформе.", + "analytics": { + "title": "Быстрая настройка Firebase Analytics", + "subtitle": "Делайте шаги строго по порядку, так интеграция будет работать стабильно.", + "step1": "Создайте один общий wrapper: shared/src/commonMain/kotlin/AnalyticsReporter.kt.", + "step2": "В logEvent(...) сначала вызовите Firebase SDK, потом Kick.firebaseAnalytics.logEvent(name, params).", + "step3": "В setUserId(...) сначала вызовите Firebase SDK, потом Kick.firebaseAnalytics.setUserId(id).", + "step4": "В setUserProperty(...) сначала вызовите Firebase SDK, потом Kick.firebaseAnalytics.setUserProperty(name, value).", + "step5": "По всему приложению используйте только этот wrapper, а не прямые вызовы Firebase." + }, + "guide": { + "title": "Что подключить в коде приложения", + "subtitle": "Выполняйте шаги ниже. Показаны только пункты для выбранных модулей.", + "logging": "Чтобы отправлять логи в модуль Logging, вызывайте Kick.log(level, message) в нужных местах.", + "loggingWithNapier": "Чтобы отправлять логи в модуль Logging, вызывайте Kick.log(level, message) в нужных местах или перенаправьте все логи Napier через installNapierBridge() из KickBootstrap.", + "ktor3": "Чтобы логировать сетевую активность Ktor3, добавьте install(KickKtor3Plugin) при создании HttpClient.", + "firebaseCloudMessagingBoth": "Для Firebase Cloud Messaging: на Android вызовите Kick.firebaseCloudMessaging.handleFcm(message) внутри FirebaseMessagingService.onMessageReceived(...). На iOS вызовите KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo) в AppDelegate при получении APNS payload.", + "firebaseCloudMessagingAndroid": "Для Firebase Cloud Messaging на Android: вызовите Kick.firebaseCloudMessaging.handleFcm(message) внутри FirebaseMessagingService.onMessageReceived(...).", + "firebaseCloudMessagingIos": "Для Firebase Cloud Messaging на iOS: вызовите KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo) в AppDelegate при получении APNS payload.", + "firebaseCloudMessagingGeneric": "Для Firebase Cloud Messaging пробрасывайте push payload в Kick-обработчики в platform lifecycle коде.", + "firebaseAnalytics": "Для Firebase Analytics добавьте вызовы Kick.firebaseAnalytics.logEvent(...), Kick.firebaseAnalytics.setUserId(...), Kick.firebaseAnalytics.setUserProperty(...) в соответствующие места вашего analytics-wrapper.", + "controlPanel": "Получайте значения ControlPanel в коде через Kick.controlPanel.getBoolean(...), Kick.controlPanel.getString(...) и т.д. Если добавили кнопки, отслеживайте клики через Kick.controlPanel.events.collect { event -> ... }.", + "overlay": "Показывайте нужные runtime-значения в плавающем окне Overlay через Kick.overlay.set(\"key\", value).", + "runner": "Добавляйте нужные debug-вызовы в нужные места проекта через Kick.runner.addCall(...)." + } + }, + "toast": { + "copied": "Скопировано", + "copyFailed": "Не удалось скопировать" + }, + "errors": { + "loadFailed": "Не удалось загрузить ресурсы мастера." + }, + "module": { + "sqldelight": { + "title": "SQLDelight", + "description": "Просмотр и редактирование SQLDelight-баз.", + "fallback": "использовать только на поддерживаемых таргетах или подключить stub/no-op на неподдерживаемых" + }, + "room": { + "title": "Room", + "description": "Просмотр таблиц и значений Room.", + "fallback": "для Web использовать SQLDelight или сделать no-op fallback для Room" + }, + "logging": { + "title": "Logging", + "description": "Live-просмотр логов с фильтрацией.", + "fallback": "отключить модуль на неподдерживаемых таргетах или использовать stub/no-op" + }, + "ktor3": { + "title": "Ktor3", + "description": "Мониторинг Ktor-запросов и ответов.", + "fallback": "отключить модуль на неподдерживаемых таргетах или использовать stub/no-op" + }, + "control_panel": { + "title": "Control Panel", + "description": "Runtime-переключатели и редактируемые debug-значения.", + "fallback": "отключить модуль на неподдерживаемых таргетах или использовать control-panel-stub" + }, + "multiplatform_settings": { + "title": "Settings", + "description": "Просмотр и редактирование Multiplatform Settings.", + "fallback": "отключить модуль на неподдерживаемых таргетах или использовать multiplatform-settings-stub" + }, + "file_explorer": { + "title": "File Explorer", + "description": "Просмотр файлов приложения.", + "fallback": "отключить модуль на неподдерживаемых таргетах или использовать file-explorer-stub" + }, + "layout": { + "title": "Layout (Beta)", + "description": "Инспекция иерархии UI и свойств узлов.", + "fallback": "не использовать Layout на неподдерживаемых платформах (например WasmJS)" + }, + "overlay": { + "title": "Overlay", + "description": "Плавающая панель live-метрик поверх UI.", + "fallback": "отключить overlay на неподдерживаемых таргетах или использовать overlay-stub" + }, + "firebase_cloud_messaging": { + "title": "Firebase Cloud Messaging", + "description": "Инспекция push-сообщений и токенов.", + "fallback": "оставить модуль только на Android/iOS; на JVM/Wasm использовать stub/no-op" + }, + "firebase_analytics": { + "title": "Firebase Analytics", + "description": "Инспекция analytics events, user id и свойств.", + "fallback": "оставить модуль только на Android/iOS; на JVM/Wasm использовать stub/no-op" + }, + "runner": { + "title": "Runner", + "description": "Запуск debug-экшенов и просмотр результатов.", + "fallback": "отключить модуль на неподдерживаемых таргетах или использовать runner-stub" + } + } +} diff --git a/content/wizard/i18n/zh-Hans.json b/content/wizard/i18n/zh-Hans.json new file mode 100644 index 00000000..cee72ecb --- /dev/null +++ b/content/wizard/i18n/zh-Hans.json @@ -0,0 +1,274 @@ +{ + "app": { + "pageTitle": "Kick 集成向导", + "wizardTitle": "集成向导" + }, + "language": { + "label": "语言", + "en": "英语", + "ru": "俄语", + "es": "西班牙语", + "pt": "葡萄牙语", + "zh-Hans": "简体中文", + "ja": "日语" + }, + "nav": { + "back": "返回", + "next": "下一步", + "nextModule": "下一步", + "done": "完成", + "reset": "重置", + "resetConfirm": "重置向导状态并清除已保存的选择?" + }, + "progress": { + "step": "第 {current} 步,共 {total} 步" + }, + "summary": { + "platforms": "平台", + "modules": "模块", + "version": "Kick 版本", + "count": "已选择 {count} 项" + }, + "step": { + "welcome": { + "title": "几分钟生成 Kick 集成", + "subtitle": "选择语言、平台和模块,生成可直接粘贴的 Gradle + Kotlin 代码片段。", + "checklistTitle": "向导流程", + "checklistSubtitle": "输出页会把所有选择整合为一份集成草稿。", + "itemLanguage": "选择界面语言", + "itemPlatforms": "选择应用平台", + "itemModules": "选择模块并配置每个已选模块", + "itemOutput": "复制 Gradle、Common Kotlin,以及可选的 Platform glue" + }, + "platforms": { + "title": "选择平台", + "subtitle": "请选择你应用当前支持的所有平台。" + }, + "modules": { + "title": "选择模块", + "subtitle": "选择要加入 Kick 初始化的模块。", + "selectedCount": "已选模块:{count}" + }, + "moduleConfig": { + "title": "配置:{module}" + }, + "output": { + "title": "集成输出", + "subtitle": "复制代码片段并粘贴到你的项目中。" + } + }, + "platform": { + "android": "Android", + "ios": "iOS", + "jvm": "JVM(桌面)", + "wasm": "WasmJS", + "androidDescription": "Android 应用目标", + "iosDescription": "iOS 应用目标", + "jvmDescription": "桌面 JVM 目标", + "wasmDescription": "WebAssembly JS 目标" + }, + "validation": { + "platformRequired": "继续前请至少选择一个平台。", + "moduleRequired": "继续前请至少选择一个模块。" + }, + "modules": { + "unsupportedWarning": "以下平台不支持:{platforms}", + "unsupportedFallback": "建议替代方案:{fallback}" + }, + "badge": { + "highConfiguration": "高配置" + }, + "config": { + "advanced": "高级", + "noQuestions": "该模块在默认流程中没有额外问题。", + "databaseCount": "数据库数量", + "databaseNameLabel": "显示名称 #{index}", + "displayName": "显示名称", + "storageItem": "存储 #{index}", + "storageDefaultName": "存储", + "logging": { + "integrateNapier": "集成 Napier", + "labelExtractorBracket": "使用 BracketLabelExtractor", + "labelExtractorCustom": "使用自定义标签提取器占位实现" + }, + "ktor3": { + "showBodies": "显示请求/响应 body", + "redactionKeys": "脱敏字段(逗号或换行分隔)" + }, + "overlay": { + "enablePerformanceProvider": "启用性能 provider", + "customProviders": "自定义 provider(逗号或换行分隔)" + }, + "runner": { + "generateSampleCalls": "生成示例调用" + }, + "controlPanel": { + "item": "条目 #{index}", + "name": "名称", + "typeLabel": "类型", + "category": "分类(可选)", + "editorLabel": "编辑器", + "listValues": "列表值(逗号分隔)", + "type": { + "bool": "Bool", + "int": "Int", + "string": "String", + "list": "List", + "button": "Button" + }, + "editor": { + "none": "无", + "input_number": "数字输入", + "input_string": "字符串输入", + "list": "列表编辑器" + } + } + }, + "actions": { + "copy": "复制", + "remove": "删除", + "skipModule": "不添加模块", + "addStorage": "添加存储", + "addItem": "添加条目", + "addExampleItems": "添加示例条目" + }, + "output": { + "steps": { + "gradle": "在 shared build.gradle.kts 中添加 Kick 插件:", + "file": "创建文件 {path},内容如下:", + "ktorExampleTitle": "Ktor3 集成示例", + "ktorExampleDescription": "在创建 HttpClient 时添加以下代码。", + "fcmExampleTitle": "Firebase Cloud Messaging 示例", + "fcmExampleDescription": "在平台回调中把 push payload 转发给 Kick。", + "analyticsExampleTitle": "Firebase Analytics 示例", + "analyticsExampleDescription": "在调用 Firebase SDK 的同一层 wrapper 中调用 Kick analytics 方法。", + "controlPanelExampleTitle": "Control Panel 使用示例", + "controlPanelExampleDescription": "读取 Control Panel 的值并据此执行业务逻辑。", + "overlayExampleTitle": "Overlay 更新示例", + "overlayExampleDescription": "在应用任意位置向 Overlay 推送实时数据。", + "runnerExampleTitle": "Runner 调用示例", + "runnerExampleDescription": "注册一个调试动作并返回 JSON 供查看。" + }, + "gradle": { + "title": "Gradle", + "description": "Kick 插件配置(含已选模块和覆盖选项)。" + }, + "common": { + "title": "Common Kotlin", + "description": "创建 shared/src/commonMain/kotlin/KickBootstrap.kt 并粘贴此代码。" + }, + "platformGlue": { + "title": "Platform glue", + "description": "按此清单执行;仅在生成时添加下面的平台文件。", + "pathLabel": "将此文件放到:", + "noFiles": "当前选择不需要额外的 platform glue 文件。" + } + }, + "glue": { + "init": { + "android": "在应用启动阶段调用 KickBootstrap.init(...)(Application.onCreate 或等价位置)。", + "ios": "在 iOS 生命周期使用的共享启动路径中调用 KickBootstrap.init(...)。", + "jvm": "在显示桌面窗口前调用 KickBootstrap.init(...)。", + "wasm": "在 Wasm 应用启动早期调用 KickBootstrap.init(...)。" + }, + "hook": { + "firebaseCloudMessagingAndroid": "在 FirebaseMessagingService.onMessageReceived(...) 中调用 Kick.firebaseCloudMessaging.handleFcm(message)。", + "firebaseCloudMessagingIos": "收到 APNS 通知时,通过共享 Kotlin bridge 调用 Kick.firebaseCloudMessaging.handleApns... 处理方法。", + "firebaseAnalytics": "在 analytics wrapper 中同步调用 Kick.firebaseAnalytics.logEvent / setUserId / setUserProperty。" + }, + "unsupportedModule": "{module}:该平台不支持。", + "analytics": { + "title": "Firebase Analytics 快速配置", + "subtitle": "按顺序执行这些步骤,照做即可保持集成一致性。", + "step1": "创建一个共享 wrapper 文件:shared/src/commonMain/kotlin/AnalyticsReporter.kt。", + "step2": "在 logEvent(...) 中先调用 Firebase SDK,再调用 Kick.firebaseAnalytics.logEvent(name, params)。", + "step3": "在 setUserId(...) 中先调用 Firebase SDK,再调用 Kick.firebaseAnalytics.setUserId(id)。", + "step4": "在 setUserProperty(...) 中先调用 Firebase SDK,再调用 Kick.firebaseAnalytics.setUserProperty(name, value)。", + "step5": "在整个应用中使用该 wrapper,替换直接调用 Firebase 的代码。" + }, + "guide": { + "title": "应用代码中需要接入的内容", + "subtitle": "按以下步骤操作。仅显示与你所选模块相关的条目。", + "logging": "如需将日志发送到 Logging 模块,请在需要的位置调用 Kick.log(level, message)。", + "loggingWithNapier": "如需将日志发送到 Logging 模块,请在需要位置调用 Kick.log(level, message),或在 KickBootstrap 中通过 installNapierBridge() 转发全部 Napier 日志。", + "ktor3": "要记录 Ktor3 网络活动,请在创建 HttpClient 时添加 install(KickKtor3Plugin)。", + "firebaseCloudMessagingBoth": "Firebase Cloud Messaging:Android 在 FirebaseMessagingService.onMessageReceived(...) 中调用 Kick.firebaseCloudMessaging.handleFcm(message)。iOS 在收到 APNS payload 时于 AppDelegate 调用 KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo)。", + "firebaseCloudMessagingAndroid": "Android 上的 Firebase Cloud Messaging:在 FirebaseMessagingService.onMessageReceived(...) 中调用 Kick.firebaseCloudMessaging.handleFcm(message)。", + "firebaseCloudMessagingIos": "iOS 上的 Firebase Cloud Messaging:在 AppDelegate 收到 APNS payload 时调用 KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo)。", + "firebaseCloudMessagingGeneric": "对于 Firebase Cloud Messaging,请在平台生命周期代码中把 push payload 转发给 Kick 处理器。", + "firebaseAnalytics": "对于 Firebase Analytics,请在你的 analytics wrapper 对应位置添加 Kick.firebaseAnalytics.logEvent(...)、Kick.firebaseAnalytics.setUserId(...) 和 Kick.firebaseAnalytics.setUserProperty(...)。", + "controlPanel": "在代码中通过 Kick.controlPanel.getBoolean(...)、Kick.controlPanel.getString(...) 等读取 ControlPanel 值。如添加了按钮,请通过 Kick.controlPanel.events.collect { event -> ... } 监听点击。", + "overlay": "通过 Kick.overlay.set(\"key\", value) 在 Overlay 浮窗显示所需运行时数据。", + "runner": "在需要的位置通过 Kick.runner.addCall(...) 注册调试动作。" + } + }, + "toast": { + "copied": "已复制", + "copyFailed": "复制失败" + }, + "errors": { + "loadFailed": "加载向导资源失败。" + }, + "module": { + "sqldelight": { + "title": "SQLDelight", + "description": "查看和编辑 SQLDelight 数据库。", + "fallback": "仅在受支持目标启用 SQLDelight,或在不受支持构建中使用 stub/no-op" + }, + "room": { + "title": "Room", + "description": "查看 Room 数据表和数据。", + "fallback": "在 Web 目标使用 SQLDelight,或为 Room 提供 no-op 回退" + }, + "logging": { + "title": "Logging", + "description": "支持过滤的实时日志查看器。", + "fallback": "在不支持目标禁用模块,或使用 stub/no-op" + }, + "ktor3": { + "title": "Ktor3", + "description": "监控 Ktor 请求与响应的网络模块。", + "fallback": "在不支持目标禁用模块,或使用 stub/no-op" + }, + "control_panel": { + "title": "Control Panel", + "description": "运行时功能开关与可编辑调试值。", + "fallback": "在不支持目标禁用模块,或使用 control-panel-stub" + }, + "multiplatform_settings": { + "title": "Settings", + "description": "查看和编辑 Multiplatform Settings 存储。", + "fallback": "在不支持目标禁用模块,或使用 multiplatform-settings-stub" + }, + "file_explorer": { + "title": "File Explorer", + "description": "浏览并查看应用文件。", + "fallback": "在不支持目标禁用模块,或使用 file-explorer-stub" + }, + "layout": { + "title": "Layout (Beta)", + "description": "检查 UI 层级与节点属性。", + "fallback": "在不受支持平台跳过 Layout(例如 WasmJS)" + }, + "overlay": { + "title": "Overlay", + "description": "悬浮显示应用 UI 上的实时指标面板。", + "fallback": "在不支持目标禁用 overlay,或使用 overlay-stub" + }, + "firebase_cloud_messaging": { + "title": "Firebase Cloud Messaging", + "description": "查看推送通知和 token 数据。", + "fallback": "仅在 Android/iOS 启用该模块;在 JVM/Wasm 使用 stub/no-op" + }, + "firebase_analytics": { + "title": "Firebase Analytics", + "description": "查看 analytics 事件、user id 和属性。", + "fallback": "仅在 Android/iOS 启用该模块;在 JVM/Wasm 使用 stub/no-op" + }, + "runner": { + "title": "Runner", + "description": "执行调试动作并查看渲染结果。", + "fallback": "在不支持目标禁用模块,或使用 runner-stub" + } + } +} diff --git a/content/wizard/index.html b/content/wizard/index.html new file mode 100644 index 00000000..d2ec83f2 --- /dev/null +++ b/content/wizard/index.html @@ -0,0 +1,46 @@ + + + + + + + Kick Integration Wizard + + + + +
    +
    +
    + +
    + Kick + +
    +
    +
    + + +
    +
    + +
    + +
    +
    +
    + + + +
    +
    +
    + +
    + + + + diff --git a/content/wizard/style.css b/content/wizard/style.css new file mode 100644 index 00000000..0e2752bf --- /dev/null +++ b/content/wizard/style.css @@ -0,0 +1,545 @@ +:root { + --bg: #f5f6f1; + --bg-muted: #e9ece1; + --surface: #ffffff; + --surface-soft: #f8faf6; + --text: #202820; + --text-muted: #566255; + --border: #d6ddce; + --accent: #0f766e; + --accent-strong: #0d5f59; + --accent-soft: #daf3ef; + --warn: #b45309; + --warn-soft: #fff0dc; + --danger: #be123c; + --shadow: 0 12px 28px rgba(37, 44, 33, 0.08); + --radius-m: 14px; + --radius-s: 10px; + --max-width: 980px; +} + +* { + box-sizing: border-box; +} + +html, +body { + margin: 0; + padding: 0; + min-height: 100%; + font-family: "Avenir Next", "Segoe UI Variable", "Helvetica Neue", "Noto Sans", sans-serif; + color: var(--text); + background: + radial-gradient(circle at top left, #dff0e2 0%, transparent 34%), + radial-gradient(circle at bottom right, #d8e6f3 0%, transparent 36%), + var(--bg); +} + +.page-shell { + width: min(var(--max-width), calc(100% - 32px)); + margin: 24px auto; + display: grid; + gap: 18px; +} + +.topbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-m); + padding: 14px 16px; + box-shadow: var(--shadow); +} + +.brand { + display: inline-flex; + align-items: center; + gap: 12px; +} + +.brand-logo { + width: 36px; + height: 36px; + border-radius: 10px; + background: linear-gradient(135deg, #0f766e, #22a388); + color: #f6fffb; + display: grid; + place-items: center; + font-weight: 700; +} + +.brand-text { + display: grid; + gap: 2px; +} + +.brand-text strong { + font-size: 17px; + letter-spacing: 0.2px; +} + +.brand-text span { + font-size: 13px; + color: var(--text-muted); +} + +.topbar-actions { + display: inline-flex; + gap: 12px; + align-items: center; +} + +.field-inline { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 13px; + color: var(--text-muted); +} + +select, +input, +textarea, +button { + font: inherit; +} + +select, +input, +textarea { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 10px; + color: var(--text); + padding: 8px 10px; +} + +input[type="checkbox"] { + accent-color: var(--accent); +} + +button { + border: 1px solid transparent; + border-radius: 10px; + cursor: pointer; + transition: background-color 120ms ease, color 120ms ease, border-color 120ms ease; +} + +button:disabled { + opacity: 0.45; + cursor: default; +} + +.primary-button { + background: var(--accent); + color: #f6fff9; + padding: 10px 16px; +} + +.primary-button:not(:disabled):hover { + background: var(--accent-strong); +} + +.ghost-button { + border-color: var(--border); + background: var(--surface); + color: var(--text); + padding: 9px 14px; +} + +.ghost-button:not(:disabled):hover { + background: var(--surface-soft); +} + +.danger-button { + border-color: #f2b2c7; + background: #fff0f6; + color: #a91d54; + padding: 9px 14px; +} + +.danger-button:not(:disabled):hover { + background: #ffe2ee; + border-color: #ec8bad; +} + +.content { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-m); + padding: 20px; + box-shadow: var(--shadow); + min-height: 460px; +} + +.page-title { + margin: 0; + font-size: 30px; + letter-spacing: 0.3px; +} + +.page-subtitle { + margin: 10px 0 0; + color: var(--text-muted); + line-height: 1.5; +} + +.panel { + border: 1px solid var(--border); + border-radius: var(--radius-s); + padding: 14px; + background: var(--surface-soft); +} + +.helper-text { + color: var(--text-muted); + font-size: 13px; +} + +.card-grid { + margin-top: 16px; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); + gap: 12px; +} + +.option-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 12px; + padding: 12px; + display: grid; + gap: 8px; +} + +.option-card.selected { + border-color: var(--accent); + box-shadow: 0 0 0 2px rgba(15, 118, 110, 0.18); +} + +.option-card h3 { + margin: 0; + font-size: 16px; +} + +.option-card p { + margin: 0; + color: var(--text-muted); + font-size: 13px; + line-height: 1.35; +} + +.badges { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.badge { + display: inline-flex; + align-items: center; + border-radius: 999px; + font-size: 12px; + padding: 3px 8px; + border: 1px solid var(--border); + background: #f7faf4; + color: var(--text-muted); +} + +.badge.high { + border-color: #f3c77f; + background: #fff5e6; + color: #7b4b0d; +} + +.warning-box { + border: 1px solid #f4c58c; + background: var(--warn-soft); + color: #7a4204; + border-radius: 10px; + padding: 10px; + font-size: 13px; + line-height: 1.4; +} + +.warning-inline { + color: var(--warn); + font-size: 13px; + line-height: 1.4; +} + +.wizard-footer { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-m); + padding: 12px 16px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + box-shadow: var(--shadow); +} + +.step-progress { + color: var(--text-muted); + font-size: 14px; +} + +.wizard-actions { + display: inline-flex; + gap: 10px; +} + +#skip-module-button { + margin-right: 16px; +} + +.form-grid { + margin-top: 14px; + display: grid; + gap: 12px; +} + +.form-row { + display: grid; + gap: 7px; +} + +.form-row label { + font-size: 13px; + color: var(--text-muted); +} + +.form-row textarea { + min-height: 84px; + resize: vertical; +} + +.form-inline { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} + +.section-title { + margin: 0; + font-size: 20px; +} + +.section-subtitle { + margin: 6px 0 0; + font-size: 14px; + color: var(--text-muted); +} + +.advanced { + margin-top: 12px; + border: 1px dashed var(--border); + border-radius: 10px; + padding: 10px; +} + +.advanced summary { + cursor: pointer; + font-weight: 600; + color: var(--text-muted); +} + +.dynamic-list { + margin-top: 10px; + display: grid; + gap: 10px; +} + +.dynamic-item { + border: 1px solid var(--border); + border-radius: 10px; + padding: 10px; + display: grid; + gap: 8px; + background: var(--surface); +} + +.dynamic-item-head { + display: flex; + justify-content: space-between; + align-items: center; +} + +.dynamic-item-title { + font-size: 13px; + color: var(--text-muted); +} + +.remove-link { + background: none; + border: none; + color: var(--danger); + padding: 0; +} + +.output-block { + margin-top: 16px; + border: 1px solid var(--border); + border-radius: 12px; + background: var(--surface-soft); + overflow: hidden; +} + +.output-steps { + margin: 18px 0 0; + padding-left: 22px; + display: grid; + gap: 14px; +} + +.output-step-item { + padding-left: 4px; +} + +.output-step-title, +.output-step-note { + margin: 0; + line-height: 1.45; +} + +.output-step-title { + font-weight: 600; +} + +.output-step-note { + color: var(--text); +} + +.output-step-item .output-block { + margin-top: 10px; +} + +.output-head { + display: flex; + justify-content: space-between; + align-items: center; + gap: 10px; + padding: 12px; + border-bottom: 1px solid var(--border); +} + +.output-head h3 { + margin: 0; + font-size: 17px; +} + +.output-head p { + margin: 4px 0 0; + font-size: 13px; + color: var(--text-muted); +} + +.code-view { + margin: 0; + padding: 14px; + background: #1d2330; + color: #dce3f2; + font-size: 12px; + line-height: 1.5; + overflow-x: auto; +} + +.copy-button { + padding: 7px 10px; + background: #f0f6f4; + border: 1px solid #cfdad6; + color: #2f463f; + font-size: 12px; +} + +.copy-button:hover { + background: #e5f3ef; +} + +.glue-platform { + margin-top: 10px; + padding-top: 10px; + border-top: 1px solid var(--border); +} + +.glue-platform:first-child { + margin-top: 0; + padding-top: 0; + border-top: none; +} + +.glue-platform h4 { + margin: 0 0 6px; +} + +.glue-platform ul { + margin: 0; + padding-left: 18px; + color: var(--text-muted); +} + +.toast { + position: fixed; + right: 20px; + bottom: 20px; + background: #113a34; + color: #f3fffa; + border-radius: 10px; + padding: 10px 12px; + opacity: 0; + transform: translateY(8px); + pointer-events: none; + transition: opacity 140ms ease, transform 140ms ease; +} + +.toast.visible { + opacity: 1; + transform: translateY(0); +} + +.hidden { + display: none !important; +} + +@media (max-width: 760px) { + .page-shell { + width: calc(100% - 18px); + margin: 10px auto; + gap: 12px; + } + + .topbar { + flex-direction: column; + align-items: stretch; + } + + .topbar-actions { + justify-content: space-between; + } + + .content { + padding: 16px; + } + + .page-title { + font-size: 24px; + } + + .wizard-footer { + flex-direction: column; + align-items: stretch; + } + + .wizard-actions { + width: 100%; + } + + .wizard-actions button { + flex: 1; + } +} From 1fefbe2b79304e306a26ddf9b02d107efaa7dbcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=91=D0=B0=D0=B6=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=90=D1=80?= =?UTF-8?q?=D1=82=D0=B5=D0=BC?= Date: Fri, 13 Feb 2026 23:40:07 +0300 Subject: [PATCH 2/2] Simplify README --- README.md | 645 ++------------------------------------ content/docs/Advanced.md | 649 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 681 insertions(+), 613 deletions(-) create mode 100644 content/docs/Advanced.md diff --git a/README.md b/README.md index 1528b353..dc0ae4fb 100644 --- a/README.md +++ b/README.md @@ -11,22 +11,11 @@ Less complexity, faster development, total visibility. That's Kick. ## Table of Contents - [Features](#features) -- [Usage](#usage) - [Modules](#modules) - - [Ktor3](#ktor3) - - [SQLite](#sqlite) - - [Logging](#logging) - - [Firebase Cloud Messaging](#firebase-cloud-messaging) - - [Firebase Analytics](#firebase-analytics) - - [Multiplatform Settings](#multiplatform-settings) - - [Control Panel](#control-panel) - - [File Explorer](#file-explorer) - - [Layout (Beta)](#layout) - - [Overlay](#overlay) - - [Runner](#runner) -- [Advanced Module Configuration](#advanced-module-configuration) -- [Shortcuts](#shortcuts) -- [Launching Kick](#launching-kick) +- [Integration](#integration) + - [Example](#example) + - [Wizard](#wizard) + - [Advanced](content/docs/Advanced.md) - [Contributing](#contributing) - [License](#license) @@ -38,11 +27,28 @@ Less complexity, faster development, total visibility. That's Kick. - Easy shortcuts — launch inspection tools quickly via shortcuts with a single click - Simple integration — just initialize with `Kick.init` -## Usage +## Modules + +| Module | Description | Platforms | +| --- | --- | --- | +| [Ktor3](content/docs/Advanced.md#ktor3) | Inspect HTTP traffic performed with Ktor3. | All | +| [SQLDelight](content/docs/Advanced.md#sqlite) | View and edit SQLite databases via the SQLDelight adapter. | All | +| [Room](content/docs/Advanced.md#sqlite) | View and edit SQLite databases via the Room adapter. | Android/iOS/JVM | +| [Logging](content/docs/Advanced.md#logging) | Capture and filter logs directly in the viewer. | All | +| [Firebase Cloud Messaging](content/docs/Advanced.md#firebase-cloud-messaging) | Inspect push payloads from FCM and APNS. | Android/iOS | +| [Firebase Analytics](content/docs/Advanced.md#firebase-analytics) | Track analytics events and user properties in Kick. | Android/iOS | +| [Multiplatform Settings](content/docs/Advanced.md#multiplatform-settings) | Browse and edit registered settings storages. | All | +| [Control Panel](content/docs/Advanced.md#control-panel) | Add runtime inputs and actions for debug configuration. | All | +| [File Explorer](content/docs/Advanced.md#file-explorer) | Explore app files and cached data from Kick UI. | All | +| [Layout](content/docs/Advanced.md#layout) | Inspect UI hierarchy, bounds, and view properties. | Android/iOS/JVM | +| [Overlay](content/docs/Advanced.md#overlay) | Show live debug metrics in a floating panel. | All | +| [Runner](content/docs/Advanced.md#runner) | Run ad-hoc debug calls with pluggable renderers. | All | -### Gradle plugin (recommended) +## Integration -You can use the **Kick Gradle plugin** (`ru.bartwell.kick`) to add Kick dependencies and configure Kotlin/Native framework exports automatically: +### Example + +Add the plugin and enable one module (`FileExplorer`): ```kotlin plugins { @@ -52,621 +58,34 @@ plugins { kick { enabled = KickEnabled.Auto - modules(KickModule.FileExplorer, KickModule.Ktor3) + modules(KickModule.FileExplorer) } -// Optional: enableKick(false) or -Pkick.enabled=true|false for override ``` -The plugin adds `main-core`, `main-runtime`/`main-runtime-stub` and the chosen module artifacts to `commonMain`, and sets framework `export(...)` for all Kotlin/Native targets. Order of `plugins` does not matter; Kotlin Multiplatform is required. - -### Manual setup - -Alternatively, add every module dependency in `shared/build.gradle.kts` and choose real or stub implementations using the `isRelease` flag: - -```kotlin -val isRelease = /* your logic to determine release vs. debug */ - -kotlin { - listOf( - iosX64(), - iosArm64(), - iosSimulatorArm64() - ).forEach { - it.binaries.framework { - baseName = "shared" - isStatic = true - export("ru.bartwell.kick:main-core:1.0.0") - if (isRelease) { - export("ru.bartwell.kick:main-runtime-stub:1.0.0") - } else { - export("ru.bartwell.kick:main-runtime:1.0.0") - } - } - } - - sourceSets { - commonMain.dependencies { - implementation("ru.bartwell.kick:main-core:1.0.0") - if (isRelease) { - implementation("ru.bartwell.kick:main-runtime-stub:1.0.0") - implementation("ru.bartwell.kick:ktor3-stub:1.0.0") - implementation("ru.bartwell.kick:sqlite-runtime-stub:1.0.0") - implementation("ru.bartwell.kick:sqlite-sqldelight-adapter-stub:1.0.0") - implementation("ru.bartwell.kick:sqlite-room-adapter-stub:1.0.0") - implementation("ru.bartwell.kick:logging-stub:1.0.0") - implementation("ru.bartwell.kick:multiplatform-settings-stub:1.0.0") - implementation("ru.bartwell.kick:file-explorer-stub:1.0.0") - implementation("ru.bartwell.kick:layout-stub:1.0.0") - implementation("ru.bartwell.kick:firebase-cloud-messaging-stub:1.0.0") - implementation("ru.bartwell.kick:firebase-analytics-stub:1.0.0") - } else { - implementation("ru.bartwell.kick:main-runtime:1.0.0") - implementation("ru.bartwell.kick:ktor3:1.0.0") - implementation("ru.bartwell.kick:sqlite-core:1.0.0") - implementation("ru.bartwell.kick:sqlite-runtime:1.0.0") - implementation("ru.bartwell.kick:sqlite-sqldelight-adapter:1.0.0") - implementation("ru.bartwell.kick:sqlite-room-adapter:1.0.0") - implementation("ru.bartwell.kick:logging:1.0.0") - implementation("ru.bartwell.kick:multiplatform-settings:1.0.0") - implementation("ru.bartwell.kick:file-explorer:1.0.0") - implementation("ru.bartwell.kick:layout:1.0.0") - implementation("ru.bartwell.kick:firebase-cloud-messaging:1.0.0") - implementation("ru.bartwell.kick:firebase-analytics:1.0.0") - } - } - } -} -``` - -**Note:** stub modules provide no-op implementations instead of the full implementations so your release build stays lightweight. - -Because many Android API calls require a Context, you need to wrap it using `PlatformContext`. Here is a sample of initialization: +Then initialize Kick with one module: ```kotlin // val context = androidContext.toPlatformContext() // For Android // val context = getPlatformContext() // For iOS and desktop // val context = platformContext() // In Compose -// let context: PlatformContext = PlatformContextKt.getPlatformContext() // For Swift Kick.init(context) { - module(SqliteModule(SqlDelightWrapper(sqlDelightDriver))) - module(SqliteModule(RoomWrapper(roomDatabase))) - module(LoggingModule(context)) - module(Ktor3Module(context)) - module(MultiplatformSettingsModule(listOf("MySettings1" to settings1, "MySettings2" to settings2))) module(FileExplorerModule()) - module(LayoutModule(context)) - module(FirebaseCloudMessagingModule(context)) - module(FirebaseAnalyticsModule(context)) -} -``` - -## Modules - -### Ktor3 - - - - - -Monitor HTTP traffic performed with Ktor3. Just install the provided plugin: - -```kotlin -val client = HttpClient(getEngineFactory()) { - install(KickKtor3Plugin) -} -``` - -### SQLite - - - - - -View and edit SQLite databases. Use one of the provided adapters (or both if you are really using Room and SqlDelight in one application) for your favorite library. - -```kotlin -// SqlDelight -module(SqliteModule(SqlDelightWrapper(sqlDelightDriver))) - -// Room -module(SqliteModule(RoomWrapper(roomDatabase))) -``` - -### Logging - - - - - -Gather and review log messages right from the viewer. Add logs with a simple call: - -```kotlin -Kick.log(LogLevel.INFO, "message") -``` - -You can also pipe existing [Napier](https://github.com/AAkira/Napier) logs into Kick so you only configure logging only once: - -```kotlin -Napier.base(object : Antilog() { - override fun performLog(priority: NapierLogLevel, tag: String?, throwable: Throwable?, message: String?) { - val level = when (priority) { - NapierLogLevel.VERBOSE -> LogLevel.VERBOSE - NapierLogLevel.DEBUG -> LogLevel.DEBUG - NapierLogLevel.INFO -> LogLevel.INFO - NapierLogLevel.WARNING -> LogLevel.WARNING - NapierLogLevel.ERROR -> LogLevel.ERROR - NapierLogLevel.ASSERT -> LogLevel.ASSERT - } - Kick.log(level, message) - } -}) -``` - -#### Labels and Filtering - -The log viewer supports two kinds of filtering: - -- Message filter - click the filter icon to filter by a text query contained in the message. -- Label filter - when a label extractor is provided, the viewer shows label chips above the list. Clicking chips toggles selected labels. Multiple selected labels are combined with AND. Label chips reflect the current text filter, so you can combine both. - -Provide a label extractor via the `LoggingModule` constructor. A ready‑to‑use `BracketLabelExtractor` is available; it extracts labels from square brackets like `[UI]`, `[Network]` in the beginning or anywhere in the message: - -```kotlin -import ru.bartwell.kick.module.logging.LoggingModule -import ru.bartwell.kick.module.logging.feature.table.util.BracketLabelExtractor - -Kick.init(context) { - module(LoggingModule(context, BracketLabelExtractor())) -} -``` - -You can also implement a custom extractor by providing your own `LabelExtractor`: - -```kotlin -import ru.bartwell.kick.module.logging.feature.table.util.LabelExtractor - -class HashLabelExtractor : LabelExtractor { - private val regex = Regex("#(\\w+)") - override fun extract(message: String?): Set = - if (message.isNullOrEmpty()) emptySet() - else regex.findAll(message).map { it.groupValues[1] }.toSet() -} - -Kick.init(context) { - module(LoggingModule(context, HashLabelExtractor())) -} -``` - -If no extractor is provided, label chips are hidden and only text filtering is available. - -### Firebase Cloud Messaging - -Capture and inspect push notifications (FCM on Android, APNS on iOS) inside Kick. - -**Enable the module:** add `FirebaseCloudMessagingModule(context)` to your `Kick.init { ... }` module list. - -**Platforms:** supported on Android and iOS. Not supported on JVM and Web. - -**Android (FCM):** call `Kick.firebaseCloudMessaging.handleFcm(message)` from your `FirebaseMessagingService.onMessageReceived`. - -```kotlin -class MyMessagingService : FirebaseMessagingService() { - override fun onMessageReceived(message: RemoteMessage) { - // your app logic... - Kick.firebaseCloudMessaging.handleFcm(message) - } -} -``` - -**iOS (APNS):** call the appropriate handler when a push is received. -Use one of the two `handleApnsPayload` overloads depending on the payload type you have (`NSDictionary` or Swift `[AnyHashable: Any]`), -or call `handleApnsNotification` if you already have a `UNNotification`. - -```swift -import UserNotifications -import shared - -final class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate { - func application( - _ application: UIApplication, - didReceiveRemoteNotification userInfo: [AnyHashable: Any], - fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void - ) { - KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo) - completionHandler(.noData) - } - - func application( - _ application: UIApplication, - didReceiveRemoteNotification userInfo: NSDictionary, - fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void - ) { - KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo) - completionHandler(.noData) - } - - func userNotificationCenter( - _ center: UNUserNotificationCenter, - didReceive response: UNNotificationResponse, - withCompletionHandler completionHandler: @escaping () -> Void - ) { - KickCompanion.shared.firebaseCloudMessaging.handleApnsNotification(notification: response.notification) - completionHandler() - } -} -``` - -### Firebase Analytics - -Capture analytics calls made by your app and inspect them inside Kick (events, user id, user properties). - -**Enable the module:** add `FirebaseAnalyticsModule(context)` to your `Kick.init { ... }` module list. - -**Platforms:** supported on Android and iOS. Not supported on JVM and Web. - -**Important:** this module does not auto-hook Firebase SDK calls. -Call `Kick.firebaseAnalytics.*` in the same places where your app already sends analytics to Firebase. - -#### Where to call it - -Use a single analytics wrapper/service in your app and call both: -- Firebase SDK (`FirebaseAnalytics` / `Analytics`) -- Kick accessor (`Kick.firebaseAnalytics`) - -This keeps instrumentation in one place and prevents missing events. - -#### Methods reference - -`Kick.firebaseAnalytics.logEvent(name, params)` -- Logs an event for the Kick viewer. -- Android signature: `logEvent(name: String, params: Bundle?)` -- iOS signatures: - - `logEvent(name: String, params: NSDictionary?)` - - `logEvent(name: String, params: Map?)` - -`Kick.firebaseAnalytics.setUserId(id)` -- Sets or clears current user id in Kick viewer (`null` clears). -- Android/iOS signature: `setUserId(id: String?)` - -`Kick.firebaseAnalytics.setUserProperty(name, value)` -- Logs user property update in Kick viewer. -- Android/iOS signature: `setUserProperty(name: String, value: String)` - -#### Android integration example - -```kotlin -class AnalyticsReporter( - private val firebaseAnalytics: FirebaseAnalytics, -) { - fun logEvent(name: String, params: Bundle?) { - firebaseAnalytics.logEvent(name, params) - Kick.firebaseAnalytics.logEvent(name, params) - } - - fun setUserId(id: String?) { - firebaseAnalytics.setUserId(id) - Kick.firebaseAnalytics.setUserId(id) - } - - fun setUserProperty(name: String, value: String) { - firebaseAnalytics.setUserProperty(name, value) - Kick.firebaseAnalytics.setUserProperty(name, value) - } -} -``` - -#### iOS integration example (Swift) - -```swift -import FirebaseAnalytics -import shared - -final class AnalyticsReporter { - func logEvent(name: String, params: [String: Any]?) { - Analytics.logEvent(name, parameters: params) - KickCompanion.shared.firebaseAnalytics.logEvent(name: name, params: params) - } - - func setUserId(_ id: String?) { - Analytics.setUserID(id) - KickCompanion.shared.firebaseAnalytics.setUserId(id: id) - } - - func setUserProperty(name: String, value: String) { - Analytics.setUserProperty(value, forName: name) - KickCompanion.shared.firebaseAnalytics.setUserProperty(name: name, value: value) - } -} -``` - -### Multiplatform Settings - - - - - -Edit values stored with [Multiplatform Settings](https://github.com/russhwolf/multiplatform-settings). Register as many storages as you need and switch between them at runtime. -**Note:** Multiplatform Settings doesn’t expose metadata about field types, so Kick can only display and edit values as plain text. When type information becomes available, it will be possible to implement type‑specific views — for example, a switch for Boolean or a numeric input for Int, Long, Double, or Float. - -### Control Panel - - - - - -Create configuration options, such as an endpoint URL or debug flags, available during app runtime. -Provide a list of `ControlPanelItem` objects to `ControlPanelModule`. Each item is either an input (`InputType`) or an action (`ActionType`). -You can optionally group items by `category` to keep long lists organized. Categories are collapsible; items without a category are shown first and are always visible. -Inputs can optionally include an editor UI: - -``` -ControlPanelModule( - context = context, - items = listOf( - ControlPanelItem( - name = "featureEnabled", - type = InputType.Boolean(true), - category = "General", - ), - ControlPanelItem( - name = "maxItems", - type = InputType.Int(DEFAULT_MAX_ITEMS), - editor = Editor.InputNumber(min = 1.0, max = 10.0), - category = "General", - ), - ControlPanelItem( - name = "endpoint", - type = InputType.String("https://example.com"), - editor = Editor.InputString(singleLine = true), - category = "Network", - ), - ControlPanelItem( - name = "list", - type = InputType.String("Item 2"), - editor = Editor.List( - listOf( - InputType.String("Item 1"), - InputType.String("Item 2"), - InputType.String("Item 3"), - ) - ), - category = "General", - ), - ControlPanelItem( - name = "Refresh Cache", - type = ActionType.Button(id = "refresh_cache"), - category = "Actions", - ), - ) -) -``` - -Access these values anywhere using the convenient `Kick.controlPanel.get*()` methods: - -``` -Kick.controlPanel.getBoolean("featureEnabled") -Kick.controlPanel.getInt("maxItems") -Kick.controlPanel.getString("endpoint") -Kick.controlPanel.getString("list") -``` - -#### Actions - -You can also add action buttons to trigger code in your app. Collect control panel events and handle button IDs you defined in `ControlPanelItem(type = ActionType.Button("id"))`: - -``` -Kick.controlPanel.events.collect { event -> - when (event) { - is ControlPanelEvent.ButtonClicked -> when (event.id) { - "refresh_cache" -> refreshCache() - } - else -> Unit - } } ``` -### File Explorer - - - - - -Browse the file system directly within the viewer—handy for quick checks of generated files or cached data. - -### Layout - - - - - -Inspect the current screen’s UI hierarchy without touching code. See a tree of views and key details like bounds, visibility, text, etc. - -Trigger it by shaking on Android and iOS or pressing ⌘⌥⇧K on macOS / Ctrl+Alt+Shift+K on Windows and Linux. Triggers work only while the module is enabled. - -### Overlay - - - - - -A small floating panel that shows live debug values over your app and updates in real time. You can drag it around or hide it at any moment. Ideal for tracking states or any quick metric while testing a scenario. - -Enable the module and update values from anywhere: +`enableKick(false)` forces stub artifacts in the current Gradle build script (equivalent to disabled mode): ```kotlin -Kick.init(context) { - module(OverlayModule(context)) -} - -// Update live values -Kick.overlay.set("fps", 42) -Kick.overlay.set("isWsConnected", true) -``` - -You can also show/hide the panel programmatically if needed: - -```kotlin -Kick.overlay.show() // show floating panel -Kick.overlay.hide() // hide it -``` - -#### Categories - -Group values by categories and switch them in the Overlay settings screen (default category is "Default"). The floating window shows only the values of the currently selected category. The selection is persisted across app restarts. - -```kotlin -// Write values into a specific category -Kick.overlay.set("fps", 42, "Performance") -Kick.overlay.set("isWsConnected", true, "Network") +enableKick(false) ``` -#### Providers +`-Pkick.enabled=true|false` has highest priority and overrides both `enableKick(...)` and `kick { enabled = ... }`. -Overlay modules can populate categories automatically through `OverlayProvider`s. By default `OverlayModule` registers the built-in `PerformanceOverlayProvider`, which exposes CPU and memory usage in the "Performance" category whenever the floating panel is visible. +### Wizard -Pass custom providers to `OverlayModule` to emit additional metrics: - -```kotlin -Kick.init(context) { - module( - OverlayModule( - context = context, - providers = listOf( - PerformanceOverlayProvider(), - MyCustomOverlayProvider(), // implements OverlayProvider - ), - ), - ) -} -``` - -Implement `OverlayProvider` to decide when your provider should run, which categories it contributes to, and how it updates values via `Kick.overlay.set` inside the supplied coroutine scope. - -### Runner - -Run ad‑hoc debug actions from inside Kick and render their results with pluggable renderers. - -Built-in renderers: -- `JsonRunnerRenderer` — pretty-prints `String?` JSON (lenient, indented). -- `ImageRunnerRenderer` — shows `PlatformImage?` (Bitmap/UIImage/BufferedImage/ImageBitmap wrapper). -- `ObjectRunnerRenderer` — displays `Any?` via `toString()`. -You can plug in your own renderer by implementing `RunnerRenderer` (with `setResult(T)` and `@Composable fun RenderContent(...)`) and passing it to `addCall` with the matching `T`. - -Add dependencies: -```kotlin -// debug -implementation("ru.bartwell.kick:runner:1.0.0") -// release (no-op) -implementation("ru.bartwell.kick:runner-stub:1.0.0") -``` - -Initialize: -```kotlin -Kick.init(context) { - module(RunnerModule()) -} -``` - -Register actions: -```kotlin -Kick.runner.addCall( - title = "Show JSON", - description = "Pretty print payload", - renderer = JsonRunnerRenderer() -) { - """{"status":"ok","ts":${System.currentTimeMillis()}}""" -} -``` - -Platform images: -- Create with `PlatformImage.fromImageBitmap(imageBitmap)` or `PlatformImage.fromNative(native)` (Bitmap/UIImage/BufferedImage). -- Render via `ImageRunnerRenderer`. - -### Advanced Module Configuration - -You don't need to add all the available modules. Just include the ones you need. Here only logging and network inspection are enabled: - -```kotlin -val isRelease = /* your logic to determine release vs. debug */ - - if (isRelease) { - implementation("ru.bartwell.kick:logging-stub:1.0.0") - implementation("ru.bartwell.kick:ktor3-stub:1.0.0") - } else { - implementation("ru.bartwell.kick:logging:1.0.0") - implementation("ru.bartwell.kick:ktor3:1.0.0") - } -``` - -```kotlin -Kick.init(context) { - module(LoggingModule(context)) - module(Ktor3Module(context)) -} -``` - -### Launching Kick - -Call `Kick.launch(context)` whenever you want to open the viewer: - -In Kotlin: - -```kotlin -val context = platformContext() -Button( - onClick = { Kick.launch(context) }, - content = { Text(text = "Kick") } -) -``` - -In Swift: - -```swift -Button("Kick") { - KickKt.shared.launch(context: PlatformContextKt.getPlatformContext()) -} -``` - -To close the viewer programmatically, call `Kick.close()`: - -In Kotlin: - -```kotlin -Kick.close() -``` - -In Swift: - -```swift -KickKt.shared.close() -``` - -## Shortcuts - -By default, Kick adds a shortcut to your app’s launcher icon (accessible via long-press). To disable it, pass `enableShortcut = false` during initialization: - -```kotlin -Kick.init(context) { - enableShortcut = false - // modules... -} -``` - -On iOS you need to configure your `AppDelegate` or `UISceneDelegate` as follows: - -```swift -class AppDelegate: NSObject, UIApplicationDelegate { - func application( - _ application: UIApplication, - configurationForConnecting connectingSceneSession: UISceneSession, - options: UIScene.ConnectionOptions - ) -> UISceneConfiguration { - return ShortcutActionHandler.shared.getConfiguration(session: connectingSceneSession) - } -} -``` +Use [Integration Wizard](content/wizard/index.html) to generate ready-to-paste plugin configuration and initialization snippets for selected modules and platforms. -Desktop (Windows/macOS/Linux): when supported by the OS, Kick also adds a System Tray icon with the label "Inspect with Kick". Clicking the tray icon launches the viewer. The icon is removed automatically when the host app exits. This tray shortcut respects the same `enableShortcut` flag — set it to `false` to disable the icon. +For full setup details (manual integration, advanced configuration, shortcuts, launching, and full module docs), see [Advanced](content/docs/Advanced.md). ## Contributing diff --git a/content/docs/Advanced.md b/content/docs/Advanced.md new file mode 100644 index 00000000..a31fb33c --- /dev/null +++ b/content/docs/Advanced.md @@ -0,0 +1,649 @@ +## Table of Contents + +- [Usage](#usage) +- [Modules](#modules) + - [Ktor3](#ktor3) + - [SQLite](#sqlite) + - [Logging](#logging) + - [Firebase Cloud Messaging](#firebase-cloud-messaging) + - [Firebase Analytics](#firebase-analytics) + - [Multiplatform Settings](#multiplatform-settings) + - [Control Panel](#control-panel) + - [File Explorer](#file-explorer) + - [Layout (Beta)](#layout) + - [Overlay](#overlay) + - [Runner](#runner) +- [Advanced Module Configuration](#advanced-module-configuration) +- [Shortcuts](#shortcuts) +- [Launching Kick](#launching-kick) + +## Usage + +### Gradle plugin (recommended) + +You can use the **Kick Gradle plugin** (`ru.bartwell.kick`) to add Kick dependencies and configure Kotlin/Native framework exports automatically: + +```kotlin +plugins { + id("org.jetbrains.kotlin.multiplatform") version "2.1.21" + id("ru.bartwell.kick") version "1.0.0" +} + +kick { + enabled = KickEnabled.Auto + modules(KickModule.FileExplorer, KickModule.Ktor3) +} +// Optional: enableKick(false) or -Pkick.enabled=true|false for override +``` + +The plugin adds `main-core`, `main-runtime`/`main-runtime-stub` and the chosen module artifacts to `commonMain`, and sets framework `export(...)` for all Kotlin/Native targets. Order of `plugins` does not matter; Kotlin Multiplatform is required. + +### Manual setup + +Alternatively, add every module dependency in `shared/build.gradle.kts` and choose real or stub implementations using the `isRelease` flag: + +```kotlin +val isRelease = /* your logic to determine release vs. debug */ + +kotlin { + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ).forEach { + it.binaries.framework { + baseName = "shared" + isStatic = true + export("ru.bartwell.kick:main-core:1.0.0") + if (isRelease) { + export("ru.bartwell.kick:main-runtime-stub:1.0.0") + } else { + export("ru.bartwell.kick:main-runtime:1.0.0") + } + } + } + + sourceSets { + commonMain.dependencies { + implementation("ru.bartwell.kick:main-core:1.0.0") + if (isRelease) { + implementation("ru.bartwell.kick:main-runtime-stub:1.0.0") + implementation("ru.bartwell.kick:ktor3-stub:1.0.0") + implementation("ru.bartwell.kick:sqlite-runtime-stub:1.0.0") + implementation("ru.bartwell.kick:sqlite-sqldelight-adapter-stub:1.0.0") + implementation("ru.bartwell.kick:sqlite-room-adapter-stub:1.0.0") + implementation("ru.bartwell.kick:logging-stub:1.0.0") + implementation("ru.bartwell.kick:multiplatform-settings-stub:1.0.0") + implementation("ru.bartwell.kick:file-explorer-stub:1.0.0") + implementation("ru.bartwell.kick:layout-stub:1.0.0") + implementation("ru.bartwell.kick:firebase-cloud-messaging-stub:1.0.0") + implementation("ru.bartwell.kick:firebase-analytics-stub:1.0.0") + } else { + implementation("ru.bartwell.kick:main-runtime:1.0.0") + implementation("ru.bartwell.kick:ktor3:1.0.0") + implementation("ru.bartwell.kick:sqlite-core:1.0.0") + implementation("ru.bartwell.kick:sqlite-runtime:1.0.0") + implementation("ru.bartwell.kick:sqlite-sqldelight-adapter:1.0.0") + implementation("ru.bartwell.kick:sqlite-room-adapter:1.0.0") + implementation("ru.bartwell.kick:logging:1.0.0") + implementation("ru.bartwell.kick:multiplatform-settings:1.0.0") + implementation("ru.bartwell.kick:file-explorer:1.0.0") + implementation("ru.bartwell.kick:layout:1.0.0") + implementation("ru.bartwell.kick:firebase-cloud-messaging:1.0.0") + implementation("ru.bartwell.kick:firebase-analytics:1.0.0") + } + } + } +} +``` + +**Note:** stub modules provide no-op implementations instead of the full implementations so your release build stays lightweight. + +Because many Android API calls require a Context, you need to wrap it using `PlatformContext`. Here is a sample of initialization: + +```kotlin +// val context = androidContext.toPlatformContext() // For Android +// val context = getPlatformContext() // For iOS and desktop +// val context = platformContext() // In Compose +// let context: PlatformContext = PlatformContextKt.getPlatformContext() // For Swift +Kick.init(context) { + module(SqliteModule(SqlDelightWrapper(sqlDelightDriver))) + module(SqliteModule(RoomWrapper(roomDatabase))) + module(LoggingModule(context)) + module(Ktor3Module(context)) + module(MultiplatformSettingsModule(listOf("MySettings1" to settings1, "MySettings2" to settings2))) + module(FileExplorerModule()) + module(LayoutModule(context)) + module(FirebaseCloudMessagingModule(context)) + module(FirebaseAnalyticsModule(context)) +} +``` + +## Modules + +### Ktor3 + + + + + +Monitor HTTP traffic performed with Ktor3. Just install the provided plugin: + +```kotlin +val client = HttpClient(getEngineFactory()) { + install(KickKtor3Plugin) +} +``` + +### SQLite + + + + + +View and edit SQLite databases. Use one of the provided adapters (or both if you are really using Room and SqlDelight in one application) for your favorite library. + +```kotlin +// SqlDelight +module(SqliteModule(SqlDelightWrapper(sqlDelightDriver))) + +// Room +module(SqliteModule(RoomWrapper(roomDatabase))) +``` + +### Logging + + + + + +Gather and review log messages right from the viewer. Add logs with a simple call: + +```kotlin +Kick.log(LogLevel.INFO, "message") +``` + +You can also pipe existing [Napier](https://github.com/AAkira/Napier) logs into Kick so you only configure logging only once: + +```kotlin +Napier.base(object : Antilog() { + override fun performLog(priority: NapierLogLevel, tag: String?, throwable: Throwable?, message: String?) { + val level = when (priority) { + NapierLogLevel.VERBOSE -> LogLevel.VERBOSE + NapierLogLevel.DEBUG -> LogLevel.DEBUG + NapierLogLevel.INFO -> LogLevel.INFO + NapierLogLevel.WARNING -> LogLevel.WARNING + NapierLogLevel.ERROR -> LogLevel.ERROR + NapierLogLevel.ASSERT -> LogLevel.ASSERT + } + Kick.log(level, message) + } +}) +``` + +#### Labels and Filtering + +The log viewer supports two kinds of filtering: + +- Message filter - click the filter icon to filter by a text query contained in the message. +- Label filter - when a label extractor is provided, the viewer shows label chips above the list. Clicking chips toggles selected labels. Multiple selected labels are combined with AND. Label chips reflect the current text filter, so you can combine both. + +Provide a label extractor via the `LoggingModule` constructor. A ready‑to‑use `BracketLabelExtractor` is available; it extracts labels from square brackets like `[UI]`, `[Network]` in the beginning or anywhere in the message: + +```kotlin +import ru.bartwell.kick.module.logging.LoggingModule +import ru.bartwell.kick.module.logging.feature.table.util.BracketLabelExtractor + +Kick.init(context) { + module(LoggingModule(context, BracketLabelExtractor())) +} +``` + +You can also implement a custom extractor by providing your own `LabelExtractor`: + +```kotlin +import ru.bartwell.kick.module.logging.feature.table.util.LabelExtractor + +class HashLabelExtractor : LabelExtractor { + private val regex = Regex("#(\\w+)") + override fun extract(message: String?): Set = + if (message.isNullOrEmpty()) emptySet() + else regex.findAll(message).map { it.groupValues[1] }.toSet() +} + +Kick.init(context) { + module(LoggingModule(context, HashLabelExtractor())) +} +``` + +If no extractor is provided, label chips are hidden and only text filtering is available. + +### Firebase Cloud Messaging + +Capture and inspect push notifications (FCM on Android, APNS on iOS) inside Kick. + +**Enable the module:** add `FirebaseCloudMessagingModule(context)` to your `Kick.init { ... }` module list. + +**Platforms:** supported on Android and iOS. Not supported on JVM and Web. + +**Android (FCM):** call `Kick.firebaseCloudMessaging.handleFcm(message)` from your `FirebaseMessagingService.onMessageReceived`. + +```kotlin +class MyMessagingService : FirebaseMessagingService() { + override fun onMessageReceived(message: RemoteMessage) { + // your app logic... + Kick.firebaseCloudMessaging.handleFcm(message) + } +} +``` + +**iOS (APNS):** call the appropriate handler when a push is received. +Use one of the two `handleApnsPayload` overloads depending on the payload type you have (`NSDictionary` or Swift `[AnyHashable: Any]`), +or call `handleApnsNotification` if you already have a `UNNotification`. + +```swift +import UserNotifications +import shared + +final class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate { + func application( + _ application: UIApplication, + didReceiveRemoteNotification userInfo: [AnyHashable: Any], + fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void + ) { + KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo) + completionHandler(.noData) + } + + func application( + _ application: UIApplication, + didReceiveRemoteNotification userInfo: NSDictionary, + fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void + ) { + KickCompanion.shared.firebaseCloudMessaging.handleApnsPayload(userInfo: userInfo) + completionHandler(.noData) + } + + func userNotificationCenter( + _ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void + ) { + KickCompanion.shared.firebaseCloudMessaging.handleApnsNotification(notification: response.notification) + completionHandler() + } +} +``` + +### Firebase Analytics + +Capture analytics calls made by your app and inspect them inside Kick (events, user id, user properties). + +**Enable the module:** add `FirebaseAnalyticsModule(context)` to your `Kick.init { ... }` module list. + +**Platforms:** supported on Android and iOS. Not supported on JVM and Web. + +**Important:** this module does not auto-hook Firebase SDK calls. +Call `Kick.firebaseAnalytics.*` in the same places where your app already sends analytics to Firebase. + +#### Where to call it + +Use a single analytics wrapper/service in your app and call both: +- Firebase SDK (`FirebaseAnalytics` / `Analytics`) +- Kick accessor (`Kick.firebaseAnalytics`) + +This keeps instrumentation in one place and prevents missing events. + +#### Methods reference + +`Kick.firebaseAnalytics.logEvent(name, params)` +- Logs an event for the Kick viewer. +- Android signature: `logEvent(name: String, params: Bundle?)` +- iOS signatures: + - `logEvent(name: String, params: NSDictionary?)` + - `logEvent(name: String, params: Map?)` + +`Kick.firebaseAnalytics.setUserId(id)` +- Sets or clears current user id in Kick viewer (`null` clears). +- Android/iOS signature: `setUserId(id: String?)` + +`Kick.firebaseAnalytics.setUserProperty(name, value)` +- Logs user property update in Kick viewer. +- Android/iOS signature: `setUserProperty(name: String, value: String)` + +#### Android integration example + +```kotlin +class AnalyticsReporter( + private val firebaseAnalytics: FirebaseAnalytics, +) { + fun logEvent(name: String, params: Bundle?) { + firebaseAnalytics.logEvent(name, params) + Kick.firebaseAnalytics.logEvent(name, params) + } + + fun setUserId(id: String?) { + firebaseAnalytics.setUserId(id) + Kick.firebaseAnalytics.setUserId(id) + } + + fun setUserProperty(name: String, value: String) { + firebaseAnalytics.setUserProperty(name, value) + Kick.firebaseAnalytics.setUserProperty(name, value) + } +} +``` + +#### iOS integration example (Swift) + +```swift +import FirebaseAnalytics +import shared + +final class AnalyticsReporter { + func logEvent(name: String, params: [String: Any]?) { + Analytics.logEvent(name, parameters: params) + KickCompanion.shared.firebaseAnalytics.logEvent(name: name, params: params) + } + + func setUserId(_ id: String?) { + Analytics.setUserID(id) + KickCompanion.shared.firebaseAnalytics.setUserId(id: id) + } + + func setUserProperty(name: String, value: String) { + Analytics.setUserProperty(value, forName: name) + KickCompanion.shared.firebaseAnalytics.setUserProperty(name: name, value: value) + } +} +``` + +### Multiplatform Settings + + + + + +Edit values stored with [Multiplatform Settings](https://github.com/russhwolf/multiplatform-settings). Register as many storages as you need and switch between them at runtime. +**Note:** Multiplatform Settings doesn’t expose metadata about field types, so Kick can only display and edit values as plain text. When type information becomes available, it will be possible to implement type‑specific views — for example, a switch for Boolean or a numeric input for Int, Long, Double, or Float. + +### Control Panel + + + + + +Create configuration options, such as an endpoint URL or debug flags, available during app runtime. +Provide a list of `ControlPanelItem` objects to `ControlPanelModule`. Each item is either an input (`InputType`) or an action (`ActionType`). +You can optionally group items by `category` to keep long lists organized. Categories are collapsible; items without a category are shown first and are always visible. +Inputs can optionally include an editor UI: + +``` +ControlPanelModule( + context = context, + items = listOf( + ControlPanelItem( + name = "featureEnabled", + type = InputType.Boolean(true), + category = "General", + ), + ControlPanelItem( + name = "maxItems", + type = InputType.Int(DEFAULT_MAX_ITEMS), + editor = Editor.InputNumber(min = 1.0, max = 10.0), + category = "General", + ), + ControlPanelItem( + name = "endpoint", + type = InputType.String("https://example.com"), + editor = Editor.InputString(singleLine = true), + category = "Network", + ), + ControlPanelItem( + name = "list", + type = InputType.String("Item 2"), + editor = Editor.List( + listOf( + InputType.String("Item 1"), + InputType.String("Item 2"), + InputType.String("Item 3"), + ) + ), + category = "General", + ), + ControlPanelItem( + name = "Refresh Cache", + type = ActionType.Button(id = "refresh_cache"), + category = "Actions", + ), + ) +) +``` + +Access these values anywhere using the convenient `Kick.controlPanel.get*()` methods: + +``` +Kick.controlPanel.getBoolean("featureEnabled") +Kick.controlPanel.getInt("maxItems") +Kick.controlPanel.getString("endpoint") +Kick.controlPanel.getString("list") +``` + +#### Actions + +You can also add action buttons to trigger code in your app. Collect control panel events and handle button IDs you defined in `ControlPanelItem(type = ActionType.Button("id"))`: + +``` +Kick.controlPanel.events.collect { event -> + when (event) { + is ControlPanelEvent.ButtonClicked -> when (event.id) { + "refresh_cache" -> refreshCache() + } + else -> Unit + } +} +``` + +### File Explorer + + + + + +Browse the file system directly within the viewer—handy for quick checks of generated files or cached data. + +### Layout + + + + + +Inspect the current screen’s UI hierarchy without touching code. See a tree of views and key details like bounds, visibility, text, etc. + +Trigger it by shaking on Android and iOS or pressing ⌘⌥⇧K on macOS / Ctrl+Alt+Shift+K on Windows and Linux. Triggers work only while the module is enabled. + +### Overlay + + + + + +A small floating panel that shows live debug values over your app and updates in real time. You can drag it around or hide it at any moment. Ideal for tracking states or any quick metric while testing a scenario. + +Enable the module and update values from anywhere: + +```kotlin +Kick.init(context) { + module(OverlayModule(context)) +} + +// Update live values +Kick.overlay.set("fps", 42) +Kick.overlay.set("isWsConnected", true) +``` + +You can also show/hide the panel programmatically if needed: + +```kotlin +Kick.overlay.show() // show floating panel +Kick.overlay.hide() // hide it +``` + +#### Categories + +Group values by categories and switch them in the Overlay settings screen (default category is "Default"). The floating window shows only the values of the currently selected category. The selection is persisted across app restarts. + +```kotlin +// Write values into a specific category +Kick.overlay.set("fps", 42, "Performance") +Kick.overlay.set("isWsConnected", true, "Network") +``` + +#### Providers + +Overlay modules can populate categories automatically through `OverlayProvider`s. By default `OverlayModule` registers the built-in `PerformanceOverlayProvider`, which exposes CPU and memory usage in the "Performance" category whenever the floating panel is visible. + +Pass custom providers to `OverlayModule` to emit additional metrics: + +```kotlin +Kick.init(context) { + module( + OverlayModule( + context = context, + providers = listOf( + PerformanceOverlayProvider(), + MyCustomOverlayProvider(), // implements OverlayProvider + ), + ), + ) +} +``` + +Implement `OverlayProvider` to decide when your provider should run, which categories it contributes to, and how it updates values via `Kick.overlay.set` inside the supplied coroutine scope. + +### Runner + +Run ad‑hoc debug actions from inside Kick and render their results with pluggable renderers. + +Built-in renderers: +- `JsonRunnerRenderer` — pretty-prints `String?` JSON (lenient, indented). +- `ImageRunnerRenderer` — shows `PlatformImage?` (Bitmap/UIImage/BufferedImage/ImageBitmap wrapper). +- `ObjectRunnerRenderer` — displays `Any?` via `toString()`. +You can plug in your own renderer by implementing `RunnerRenderer` (with `setResult(T)` and `@Composable fun RenderContent(...)`) and passing it to `addCall` with the matching `T`. + +Add dependencies: +```kotlin +// debug +implementation("ru.bartwell.kick:runner:1.0.0") +// release (no-op) +implementation("ru.bartwell.kick:runner-stub:1.0.0") +``` + +Initialize: +```kotlin +Kick.init(context) { + module(RunnerModule()) +} +``` + +Register actions: +```kotlin +Kick.runner.addCall( + title = "Show JSON", + description = "Pretty print payload", + renderer = JsonRunnerRenderer() +) { + """{"status":"ok","ts":${System.currentTimeMillis()}}""" +} +``` + +Platform images: +- Create with `PlatformImage.fromImageBitmap(imageBitmap)` or `PlatformImage.fromNative(native)` (Bitmap/UIImage/BufferedImage). +- Render via `ImageRunnerRenderer`. + +### Advanced Module Configuration + +You don't need to add all the available modules. Just include the ones you need. Here only logging and network inspection are enabled: + +```kotlin +val isRelease = /* your logic to determine release vs. debug */ + + if (isRelease) { + implementation("ru.bartwell.kick:logging-stub:1.0.0") + implementation("ru.bartwell.kick:ktor3-stub:1.0.0") + } else { + implementation("ru.bartwell.kick:logging:1.0.0") + implementation("ru.bartwell.kick:ktor3:1.0.0") + } +``` + +```kotlin +Kick.init(context) { + module(LoggingModule(context)) + module(Ktor3Module(context)) +} +``` + +### Launching Kick + +Call `Kick.launch(context)` whenever you want to open the viewer: + +In Kotlin: + +```kotlin +val context = platformContext() +Button( + onClick = { Kick.launch(context) }, + content = { Text(text = "Kick") } +) +``` + +In Swift: + +```swift +Button("Kick") { + KickKt.shared.launch(context: PlatformContextKt.getPlatformContext()) +} +``` + +To close the viewer programmatically, call `Kick.close()`: + +In Kotlin: + +```kotlin +Kick.close() +``` + +In Swift: + +```swift +KickKt.shared.close() +``` + +## Shortcuts + +By default, Kick adds a shortcut to your app’s launcher icon (accessible via long-press). To disable it, pass `enableShortcut = false` during initialization: + +```kotlin +Kick.init(context) { + enableShortcut = false + // modules... +} +``` + +On iOS you need to configure your `AppDelegate` or `UISceneDelegate` as follows: + +```swift +class AppDelegate: NSObject, UIApplicationDelegate { + func application( + _ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions + ) -> UISceneConfiguration { + return ShortcutActionHandler.shared.getConfiguration(session: connectingSceneSession) + } +} +``` + +Desktop (Windows/macOS/Linux): when supported by the OS, Kick also adds a System Tray icon with the label "Inspect with Kick". Clicking the tray icon launches the viewer. The icon is removed automatically when the host app exits. This tray shortcut respects the same `enableShortcut` flag — set it to `false` to disable the icon. +