mirror of
https://github.com/fromchat-messenger/app.git
synced 2026-09-22 19:15:05 +03:00
@@ -25,16 +25,43 @@ dependencies {
|
|||||||
|
|
||||||
val desktopWindowIconPng = layout.projectDirectory.file("src/main/resources/app_window_icon.png")
|
val desktopWindowIconPng = layout.projectDirectory.file("src/main/resources/app_window_icon.png")
|
||||||
val desktopWindowIconIcns = layout.projectDirectory.file("icons/app_window_icon.icns")
|
val desktopWindowIconIcns = layout.projectDirectory.file("icons/app_window_icon.icns")
|
||||||
|
val dmgBackgroundDir = layout.projectDirectory.dir("dmg-background")
|
||||||
|
val dmgStageDir = layout.buildDirectory.dir("dmg-background/stage")
|
||||||
|
val dmgDistDir = layout.buildDirectory.dir("dmg-background/dist")
|
||||||
|
val debugAppBundle = layout.buildDirectory.dir("compose/binaries/main/app/FromChat.app")
|
||||||
|
val releaseAppBundle = layout.buildDirectory.dir("compose/binaries/main-release/app/FromChat.app")
|
||||||
|
val testDmgOutput = layout.buildDirectory.file("distributions/FromChat-test.dmg")
|
||||||
|
val releaseDmgOutput = layout.buildDirectory.file("distributions/FromChat.dmg")
|
||||||
|
|
||||||
|
fun resolvePackagingJdkHome(): String {
|
||||||
|
System.getenv("FROMCHAT_PACKAGING_JDK")?.takeIf { it.isNotBlank() }?.let { return it }
|
||||||
|
val candidates = listOf(
|
||||||
|
"${System.getProperty("user.home")}/.gradle/jdks/eclipse_adoptium-17-aarch64-os_x.2/jdk-17.0.19+10/Contents/Home",
|
||||||
|
"/Library/Java/JavaVirtualMachines/temurin-17.jdk/Contents/Home",
|
||||||
|
"/Library/Java/JavaVirtualMachines/temurin-21.jdk/Contents/Home",
|
||||||
|
"/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home",
|
||||||
|
)
|
||||||
|
return candidates.firstOrNull { File(it, "bin/jpackage").isFile }
|
||||||
|
?: error(
|
||||||
|
"No JDK with jpackage found. Install Temurin 17+ or set FROMCHAT_PACKAGING_JDK.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
compose.desktop {
|
compose.desktop {
|
||||||
application {
|
application {
|
||||||
mainClass = "ru.fromchat.desktop.MainKt"
|
mainClass = "ru.fromchat.desktop.MainKt"
|
||||||
|
javaHome = resolvePackagingJdkHome()
|
||||||
|
|
||||||
|
buildTypes.release.proguard {
|
||||||
|
configurationFiles.from(project.file("proguard-rules.pro"))
|
||||||
|
}
|
||||||
|
|
||||||
nativeDistributions {
|
nativeDistributions {
|
||||||
packageName = "FromChat"
|
packageName = "FromChat"
|
||||||
packageVersion = "1.0.0"
|
packageVersion = "1.0.0"
|
||||||
description = "FromChat desktop"
|
description = "FromChat desktop"
|
||||||
copyright = "© FromChat"
|
copyright = "© FromChat"
|
||||||
|
includeAllModules = true
|
||||||
|
|
||||||
modules(
|
modules(
|
||||||
"javafx.base",
|
"javafx.base",
|
||||||
@@ -307,3 +334,151 @@ private fun openjfxClassifier(): String {
|
|||||||
else -> "linux"
|
else -> "linux"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val runningOnMacOs = System.getProperty("os.name").orEmpty().lowercase().contains("mac")
|
||||||
|
|
||||||
|
val installDmgBackgroundTools = tasks.register<Exec>("installDmgBackgroundTools") {
|
||||||
|
onlyIf { runningOnMacOs }
|
||||||
|
workingDir = dmgBackgroundDir.asFile
|
||||||
|
inputs.file(dmgBackgroundDir.file("package.json"))
|
||||||
|
inputs.file(dmgBackgroundDir.file("package-lock.json"))
|
||||||
|
outputs.dir(dmgBackgroundDir.dir("node_modules/playwright"))
|
||||||
|
commandLine(
|
||||||
|
"bash",
|
||||||
|
"-lc",
|
||||||
|
"""
|
||||||
|
if [ ! -f node_modules/playwright/package.json ]; then
|
||||||
|
npm install --no-fund --no-audit
|
||||||
|
npx playwright install chromium
|
||||||
|
fi
|
||||||
|
""".trimIndent(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val stageDmgBackground = tasks.register<Copy>("stageDmgBackground") {
|
||||||
|
onlyIf { runningOnMacOs }
|
||||||
|
dependsOn(":app:shared:prepareComposeResourcesTaskForCommonMain")
|
||||||
|
mustRunAfter(installDmgBackgroundTools)
|
||||||
|
from(dmgBackgroundDir) {
|
||||||
|
exclude("node_modules/**")
|
||||||
|
}
|
||||||
|
into(dmgStageDir)
|
||||||
|
doLast {
|
||||||
|
val sharedLogo = rootProject.project(":app:shared").layout.buildDirectory
|
||||||
|
.file("generated/compose/resourceGenerator/preparedResources/commonMain/composeResources/drawable/logo_square.png")
|
||||||
|
.get().asFile
|
||||||
|
if (sharedLogo.isFile) {
|
||||||
|
val target = dmgStageDir.get().asFile.resolve("assets/logo_square.png")
|
||||||
|
target.parentFile.mkdirs()
|
||||||
|
sharedLogo.copyTo(target, overwrite = true)
|
||||||
|
} else {
|
||||||
|
logger.lifecycle("Compose logo not generated; using bundled dmg-background/assets/logo_square.png")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val exportDmgBackground = tasks.register<Exec>("exportDmgBackground") {
|
||||||
|
onlyIf { runningOnMacOs }
|
||||||
|
dependsOn(stageDmgBackground, installDmgBackgroundTools)
|
||||||
|
workingDir = dmgBackgroundDir.asFile
|
||||||
|
val stage = dmgStageDir.get().asFile.absolutePath
|
||||||
|
val dist = dmgDistDir.get().asFile.absolutePath
|
||||||
|
inputs.dir(dmgStageDir)
|
||||||
|
outputs.dir(dmgDistDir)
|
||||||
|
commandLine(
|
||||||
|
"bash",
|
||||||
|
"-lc",
|
||||||
|
"node export.mjs '${stage}' '${dist}'",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun org.gradle.api.tasks.TaskContainer.registerPackageDmgTask(
|
||||||
|
name: String,
|
||||||
|
appBundle: Provider<Directory>,
|
||||||
|
dmgOutput: Provider<RegularFile>,
|
||||||
|
distributableTask: String,
|
||||||
|
scriptName: String,
|
||||||
|
) = register<Exec>(name) {
|
||||||
|
onlyIf { runningOnMacOs }
|
||||||
|
group = "compose desktop"
|
||||||
|
dependsOn(distributableTask, exportDmgBackground)
|
||||||
|
val positionsFile = dmgDistDir.map { it.file("icon-positions.json") }
|
||||||
|
val dmgScript = layout.buildDirectory.file("dmg-background/$scriptName")
|
||||||
|
inputs.dir(appBundle)
|
||||||
|
inputs.dir(dmgDistDir)
|
||||||
|
inputs.file(positionsFile)
|
||||||
|
outputs.file(dmgOutput)
|
||||||
|
doFirst {
|
||||||
|
val app = appBundle.get().asFile
|
||||||
|
check(app.isDirectory) {
|
||||||
|
"Missing ${app.absolutePath}. Run :app:desktop:$distributableTask first."
|
||||||
|
}
|
||||||
|
val positions = positionsFile.get().asFile
|
||||||
|
check(positions.isFile) { "Missing ${positions.absolutePath}" }
|
||||||
|
val script = dmgScript.get().asFile
|
||||||
|
script.parentFile.mkdirs()
|
||||||
|
script.writeText(
|
||||||
|
"""
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
POSITIONS='${positions.absolutePath}'
|
||||||
|
APP='${app.absolutePath}'
|
||||||
|
OUT='${dmgOutput.get().asFile.absolutePath}'
|
||||||
|
DIST='${dmgDistDir.get().asFile.absolutePath}'
|
||||||
|
STAGING="${'$'}{TMPDIR:-/tmp}/fromchat-dmg-staging-${'$'}RANDOM"
|
||||||
|
rm -rf "${'$'}OUT" "${'$'}STAGING"
|
||||||
|
mkdir -p "${'$'}STAGING"
|
||||||
|
cp -R "${'$'}APP" "${'$'}STAGING/"
|
||||||
|
WINDOW_SIZE=($(node -e "const c=require('${'$'}POSITIONS').createDmg; console.log(c.windowSize.join(' '))"))
|
||||||
|
ICON_SIZE=$(node -e "console.log(require('${'$'}POSITIONS').createDmg.iconSize)")
|
||||||
|
APP_POS=($(node -e "const i=require('${'$'}POSITIONS').createDmg.icons.find(x=>x[0]==='FromChat.app'); console.log(i[1], i[2])"))
|
||||||
|
APPS_POS=($(node -e "const i=require('${'$'}POSITIONS').createDmg.icons.find(x=>x[0]==='Applications'); console.log(i[1], i[2])"))
|
||||||
|
(
|
||||||
|
cd "${'$'}DIST"
|
||||||
|
create-dmg \
|
||||||
|
--volname "FromChat" \
|
||||||
|
--window-size "${'$'}{WINDOW_SIZE[0]}" "${'$'}{WINDOW_SIZE[1]}" \
|
||||||
|
--icon-size "${'$'}ICON_SIZE" \
|
||||||
|
--icon "FromChat.app" "${'$'}{APP_POS[0]}" "${'$'}{APP_POS[1]}" \
|
||||||
|
--hide-extension "FromChat.app" \
|
||||||
|
--app-drop-link "${'$'}{APPS_POS[0]}" "${'$'}{APPS_POS[1]}" \
|
||||||
|
--app-drop-link-name "Программы" \
|
||||||
|
--text-size 14 \
|
||||||
|
--background "dmg-background@2x.png" \
|
||||||
|
"${'$'}OUT" \
|
||||||
|
"${'$'}STAGING"
|
||||||
|
)
|
||||||
|
rm -rf "${'$'}STAGING"
|
||||||
|
""".trimIndent() + "\n",
|
||||||
|
)
|
||||||
|
script.setExecutable(true)
|
||||||
|
commandLine(script.absolutePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.registerPackageDmgTask(
|
||||||
|
name = "packageTestDmg",
|
||||||
|
appBundle = debugAppBundle,
|
||||||
|
dmgOutput = testDmgOutput,
|
||||||
|
distributableTask = "createDistributable",
|
||||||
|
scriptName = "package-test-dmg.sh",
|
||||||
|
).configure {
|
||||||
|
description = "Build debug app bundle, export DMG background, and package FromChat-test.dmg."
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.registerPackageDmgTask(
|
||||||
|
name = "packageReleaseDmg",
|
||||||
|
appBundle = releaseAppBundle,
|
||||||
|
dmgOutput = releaseDmgOutput,
|
||||||
|
distributableTask = "createReleaseDistributable",
|
||||||
|
scriptName = "package-release-dmg.sh",
|
||||||
|
).configure {
|
||||||
|
description =
|
||||||
|
"Build release app (ProGuard + jpackage), export DMG background, and package FromChat.dmg. ProGuard may take 10–20 minutes."
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("packageDmg") {
|
||||||
|
group = "compose desktop"
|
||||||
|
description = "Alias for packageReleaseDmg (full release DMG pipeline)."
|
||||||
|
dependsOn("packageReleaseDmg")
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
"appLabel": "FromChat",
|
"appLabel": "FromChat",
|
||||||
"applicationsLabel": "Программы",
|
"applicationsLabel": "Программы",
|
||||||
"primaryColor": "#dabaf9"
|
"primaryColor": "#dabaf9",
|
||||||
|
"titleBarInset": 22,
|
||||||
|
"windowChromeBottom": 22
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ function buildPositionsJson(measured) {
|
|||||||
},
|
},
|
||||||
createDmg: {
|
createDmg: {
|
||||||
windowSize: [round(artboard.width), round(artboard.height)],
|
windowSize: [round(artboard.width), round(artboard.height)],
|
||||||
iconSize: Math.round(Math.min(app.width, app.height) * 0.86),
|
iconSize: Math.round(Math.min(app.width, app.height) * 0.88),
|
||||||
icons: [
|
icons: [
|
||||||
["FromChat.app", round(app.centerX), round(app.centerY)],
|
["FromChat.app", round(app.centerX), round(app.centerY)],
|
||||||
["Applications", round(applications.centerX), round(applications.centerY)],
|
["Applications", round(applications.centerX), round(applications.centerY)],
|
||||||
@@ -157,6 +157,20 @@ function buildPositionsJson(measured) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function applyConfig(page, config) {
|
||||||
|
const windowChromeBottom = config.windowChromeBottom ?? config.titleBarInset ?? 22;
|
||||||
|
await page.evaluate(
|
||||||
|
({ bottom, primaryColor }) => {
|
||||||
|
document.documentElement.style.setProperty("--window-chrome-bottom", `${bottom}px`);
|
||||||
|
if (primaryColor) {
|
||||||
|
document.documentElement.style.setProperty("--primary-pill", primaryColor);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ bottom: windowChromeBottom, primaryColor: config.primaryColor },
|
||||||
|
);
|
||||||
|
return windowChromeBottom;
|
||||||
|
}
|
||||||
|
|
||||||
async function applyLabelsAndMeasure(page, config, measured) {
|
async function applyLabelsAndMeasure(page, config, measured) {
|
||||||
// Set labels from config into DOM, measure their rendered widths and apply inline styles,
|
// Set labels from config into DOM, measure their rendered widths and apply inline styles,
|
||||||
// then position the label pills centered under measured slot centers.
|
// then position the label pills centered under measured slot centers.
|
||||||
@@ -188,43 +202,23 @@ async function applyLabelsAndMeasure(page, config, measured) {
|
|||||||
return w;
|
return w;
|
||||||
};
|
};
|
||||||
|
|
||||||
const horizPad = 18;
|
const horizPad = 24;
|
||||||
const appW = measureTextWidth(appLabel);
|
const appW = measureTextWidth(appLabel);
|
||||||
const appsW = measureTextWidth(appsLabel);
|
const appsW = measureTextWidth(appsLabel);
|
||||||
|
// Finder draws labels below icons; placeholders stay invisible in the PNG.
|
||||||
|
const labelGapBelowSlot = -1;
|
||||||
|
|
||||||
if (appLabel) {
|
const placeLabel = (el, slot, textWidth) => {
|
||||||
appLabel.style.width = (appW + horizPad) + "px";
|
if (!el || !slot) return;
|
||||||
appLabel.style.padding = "4px 9px";
|
const width = textWidth + horizPad;
|
||||||
}
|
el.style.width = `${width}px`;
|
||||||
if (appsLabel) {
|
el.style.left = `${Math.round(slot.centerX)}px`;
|
||||||
appsLabel.style.width = (appsW + horizPad) + "px";
|
el.style.top = `${Math.round(slot.centerY + slot.height / 2 + labelGapBelowSlot)}px`;
|
||||||
appsLabel.style.padding = "4px 9px";
|
el.style.transform = "translateX(-50%)";
|
||||||
}
|
|
||||||
|
|
||||||
// Position labels using translate-based offsets from 50% (keeps consistent centering and avoids
|
|
||||||
// coordinate-space mismatches). Compute translateX so that the pill is centered at slot.centerX.
|
|
||||||
const artboard = document.querySelector("#dmg");
|
|
||||||
const artWidth = measured?.artboard?.width || artboard.clientWidth;
|
|
||||||
const verticalOffset = 54; // px, matches previous visual placement
|
|
||||||
|
|
||||||
const computeTranslateX = (slotCenterX, labelWidth) => {
|
|
||||||
// desiredLeft = slotCenterX - labelWidth/2
|
|
||||||
// with left:50% the origin is artWidth/2, so translateX = desiredLeft - artWidth/2
|
|
||||||
return Math.round(slotCenterX - labelWidth / 2 - artWidth / 2);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (appLabel && measured?.slots?.app) {
|
placeLabel(appLabel, measured?.slots?.app, appW);
|
||||||
const s = measured.slots.app;
|
placeLabel(appsLabel, measured?.slots?.applications, appsW);
|
||||||
const tx = computeTranslateX(s.centerX, appW + horizPad);
|
|
||||||
appLabel.style.left = "50%";
|
|
||||||
appLabel.style.transform = `translate(${tx}px, ${verticalOffset}px)`;
|
|
||||||
}
|
|
||||||
if (appsLabel && measured?.slots?.applications) {
|
|
||||||
const s = measured.slots.applications;
|
|
||||||
const tx = computeTranslateX(s.centerX, appsW + horizPad);
|
|
||||||
appsLabel.style.left = "50%";
|
|
||||||
appsLabel.style.transform = `translate(${tx}px, ${verticalOffset}px)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rectFor = (el) => {
|
const rectFor = (el) => {
|
||||||
if (!el) return null;
|
if (!el) return null;
|
||||||
@@ -259,6 +253,34 @@ async function screenshotArtboard(page, outPath, deviceScaleFactor) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadConfig() {
|
||||||
|
try {
|
||||||
|
const cfgText = await readFileSync(CONFIG_PATH, "utf8");
|
||||||
|
return JSON.parse(cfgText);
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportAtScale(browser, baseUrl, cfg, scale) {
|
||||||
|
const context = await browser.newContext({
|
||||||
|
viewport: { width: 1280, height: 900 },
|
||||||
|
deviceScaleFactor: scale,
|
||||||
|
});
|
||||||
|
const page = await context.newPage();
|
||||||
|
await page.goto(baseUrl, { waitUntil: "networkidle" });
|
||||||
|
await page.evaluate(async () => {
|
||||||
|
if (document.fonts?.ready) await document.fonts.ready;
|
||||||
|
});
|
||||||
|
await applyConfig(page, cfg);
|
||||||
|
const measured = await measureSlots(page);
|
||||||
|
const labelMetrics = await applyLabelsAndMeasure(page, cfg, measured);
|
||||||
|
const suffix = scale === 2 ? "@2x" : "";
|
||||||
|
await screenshotArtboard(page, path.join(DIST, `dmg-background${suffix}.png`), scale);
|
||||||
|
await context.close();
|
||||||
|
return { measured, labelMetrics };
|
||||||
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
await mkdir(DIST, { recursive: true });
|
await mkdir(DIST, { recursive: true });
|
||||||
const { server, port } = await startStaticServer();
|
const { server, port } = await startStaticServer();
|
||||||
@@ -266,56 +288,20 @@ async function main() {
|
|||||||
let browser;
|
let browser;
|
||||||
try {
|
try {
|
||||||
browser = await chromium.launch({ headless: true });
|
browser = await chromium.launch({ headless: true });
|
||||||
|
const cfg = await loadConfig();
|
||||||
|
|
||||||
{
|
const { measured, labelMetrics } = await exportAtScale(browser, baseUrl, cfg, 1);
|
||||||
const context = await browser.newContext({
|
|
||||||
viewport: { width: 1280, height: 900 },
|
|
||||||
deviceScaleFactor: 1,
|
|
||||||
});
|
|
||||||
const page = await context.newPage();
|
|
||||||
await page.goto(baseUrl, { waitUntil: "networkidle" });
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
if (document.fonts?.ready) await document.fonts.ready;
|
|
||||||
});
|
|
||||||
// load config
|
|
||||||
let cfg = {};
|
|
||||||
try {
|
|
||||||
const cfgText = await readFileSync(CONFIG_PATH, "utf8");
|
|
||||||
cfg = JSON.parse(cfgText);
|
|
||||||
} catch (e) {
|
|
||||||
cfg = {};
|
|
||||||
}
|
|
||||||
// measure slots first
|
|
||||||
const measured = await measureSlots(page);
|
|
||||||
// apply labels/pill widths and position them based on measured slot coordinates
|
|
||||||
const labelMetrics = await applyLabelsAndMeasure(page, cfg, measured);
|
|
||||||
console.log("label metrics:", JSON.stringify(labelMetrics));
|
console.log("label metrics:", JSON.stringify(labelMetrics));
|
||||||
const positions = buildPositionsJson(measured);
|
const positions = buildPositionsJson(measured);
|
||||||
// attach label metrics to the output so build tasks can pre-calc sizes
|
|
||||||
positions.labels = labelMetrics.labels;
|
positions.labels = labelMetrics.labels;
|
||||||
await writeFile(
|
await writeFile(
|
||||||
path.join(DIST, "icon-positions.json"),
|
path.join(DIST, "icon-positions.json"),
|
||||||
`${JSON.stringify(positions, null, 2)}\n`,
|
`${JSON.stringify(positions, null, 2)}\n`,
|
||||||
"utf8",
|
"utf8",
|
||||||
);
|
);
|
||||||
await screenshotArtboard(page, path.join(DIST, "dmg-background.png"), 1);
|
|
||||||
await context.close();
|
|
||||||
console.log(`canvas ${positions.canvas.width}×${positions.canvas.height}`);
|
console.log(`canvas ${positions.canvas.width}×${positions.canvas.height}`);
|
||||||
}
|
|
||||||
|
|
||||||
{
|
await exportAtScale(browser, baseUrl, cfg, 2);
|
||||||
const context = await browser.newContext({
|
|
||||||
viewport: { width: 1280, height: 900 },
|
|
||||||
deviceScaleFactor: 2,
|
|
||||||
});
|
|
||||||
const page = await context.newPage();
|
|
||||||
await page.goto(baseUrl, { waitUntil: "networkidle" });
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
if (document.fonts?.ready) await document.fonts.ready;
|
|
||||||
});
|
|
||||||
await screenshotArtboard(page, path.join(DIST, "dmg-background@2x.png"), 2);
|
|
||||||
await context.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`wrote ${path.join(DIST, "dmg-background.png")}`);
|
console.log(`wrote ${path.join(DIST, "dmg-background.png")}`);
|
||||||
console.log(`wrote ${path.join(DIST, "dmg-background@2x.png")}`);
|
console.log(`wrote ${path.join(DIST, "dmg-background@2x.png")}`);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<div id="dmg" role="img" aria-label="Фон установки FromChat (DMG)">
|
<div id="dmg" role="img" aria-label="Фон установки FromChat (DMG)">
|
||||||
|
<div class="window-chrome-bleed" aria-hidden="true"></div>
|
||||||
<header class="brand">
|
<header class="brand">
|
||||||
<div class="brand__logo-wrap">
|
<div class="brand__logo-wrap">
|
||||||
<div class="brand__logo" aria-hidden="true"></div>
|
<div class="brand__logo" aria-hidden="true"></div>
|
||||||
@@ -25,22 +26,22 @@
|
|||||||
|
|
||||||
<div class="install">
|
<div class="install">
|
||||||
<div id="slot-app" class="slot" aria-hidden="true"></div>
|
<div id="slot-app" class="slot" aria-hidden="true"></div>
|
||||||
<div class="slot__label" data-for="slot-app" aria-hidden="true">FromChat</div>
|
|
||||||
<div class="arrow" aria-hidden="true">
|
<div class="arrow" aria-hidden="true">
|
||||||
<!-- Material Symbols Rounded arrow_forward (Google Fonts / fonts.gstatic) -->
|
<!-- Material Symbols Rounded arrow_forward (Google Fonts / fonts.gstatic) -->
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" width="36" height="36" fill="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" width="44" height="44" fill="currentColor">
|
||||||
<path d="M686-450H190q-13 0-21.5-8.5T160-480q0-13 8.5-21.5T190-510h496L459-737q-9-9-9-21t9-21q9-9 21-9t21 9l278 278q5 5 7 10t2 11q0 6-2 11t-7 10L501-181q-9 9-21 9t-21-9q-9-9-9-21t9-21l227-227Z"/>
|
<path d="M686-450H190q-13 0-21.5-8.5T160-480q0-13 8.5-21.5T190-510h496L459-737q-9-9-9-21t9-21q9-9 21-9t21 9l278 278q5 5 7 10t2 11q0 6-2 11t-7 10L501-181q-9 9-21 9t-21-9q-9-9-9-21t9-21l227-227Z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div id="slot-applications" class="slot" aria-hidden="true"></div>
|
<div id="slot-applications" class="slot" aria-hidden="true"></div>
|
||||||
<div class="slot__label" data-for="slot-applications" aria-hidden="true">Программы</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="slot__label" data-for="slot-app" aria-hidden="true">FromChat</div>
|
||||||
|
<div class="slot__label" data-for="slot-applications" aria-hidden="true">Программы</div>
|
||||||
|
|
||||||
<p class="notice">
|
<p class="notice">
|
||||||
Если macOS блокирует приложение, откройте
|
Если macOS блокирует приложение, откройте
|
||||||
<strong>Системные настройки → Конфиденциальность и безопасность</strong>
|
<strong>Системные настройки → Конфиденциальность и безопасность</strong>
|
||||||
и разрешите его — или нажмите
|
и разрешите его.
|
||||||
<strong>правой кнопкой → Открыть</strong>.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"name": "fromchat-dmg-background-tools",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "fromchat-dmg-background-tools",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "1.52.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.52.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -74,18 +74,16 @@ body {
|
|||||||
--surface: rgb(21, 18, 24);
|
--surface: rgb(21, 18, 24);
|
||||||
--on-surface: rgba(230, 224, 233, 0.92);
|
--on-surface: rgba(230, 224, 233, 0.92);
|
||||||
--on-surface-muted: rgba(230, 224, 233, 0.72);
|
--on-surface-muted: rgba(230, 224, 233, 0.72);
|
||||||
/* Slightly smaller slots and outlines so they fit naturally with labels */
|
/* Extra strip at the bottom; Finder clips it so the visible area lines up with the title bar. */
|
||||||
|
--window-chrome-bottom: 22px;
|
||||||
|
--content-height: 380px;
|
||||||
--slot-size: 96px;
|
--slot-size: 96px;
|
||||||
--slot-size: 88px;
|
|
||||||
--slot-radius: 14px;
|
|
||||||
--outline: rgba(230, 224, 233, 0.30);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#dmg {
|
#dmg {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 540px;
|
width: 540px;
|
||||||
/* increase artboard height to avoid bottom cropping on Retina displays and room for title bar */
|
height: calc(var(--content-height) + var(--window-chrome-bottom));
|
||||||
height: 380px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--on-surface);
|
color: var(--on-surface);
|
||||||
font-family: "Google Sans", system-ui, sans-serif;
|
font-family: "Google Sans", system-ui, sans-serif;
|
||||||
@@ -93,17 +91,27 @@ body {
|
|||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Clipped by Finder at the bottom — keeps the designed content aligned under the title bar. */
|
||||||
|
.window-chrome-bleed {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
height: var(--window-chrome-bottom);
|
||||||
|
background-color: var(--surface);
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.brand {
|
.brand {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
/* move brand further down to avoid title-bar overlap in Finder window */
|
top: 28px;
|
||||||
top: 44px;
|
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand__logo-wrap {
|
.brand__logo-wrap {
|
||||||
@@ -120,7 +128,6 @@ body {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
width: 1200px;
|
width: 1200px;
|
||||||
height: 1200px;
|
height: 1200px;
|
||||||
/* move radial blur down slightly to reduce pattern frequency near center */
|
|
||||||
bottom: -180px;
|
bottom: -180px;
|
||||||
opacity: 0.48;
|
opacity: 0.48;
|
||||||
left: calc(50% - 600px);
|
left: calc(50% - 600px);
|
||||||
@@ -131,13 +138,13 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.brand__logo {
|
.brand__logo {
|
||||||
width: 52px;
|
width: 68px;
|
||||||
height: 52px;
|
height: 68px;
|
||||||
background-image: url("assets/logo_square.png");
|
background-image: url("assets/logo_square.png");
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
border-radius: 13px;
|
border-radius: 17px;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
@@ -145,8 +152,7 @@ body {
|
|||||||
.brand__wordmark {
|
.brand__wordmark {
|
||||||
font-family: "Montserrat", "Google Sans", sans-serif;
|
font-family: "Montserrat", "Google Sans", sans-serif;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
/* slightly larger for Retina */
|
font-size: 30px;
|
||||||
font-size: 24px;
|
|
||||||
letter-spacing: -0.03em;
|
letter-spacing: -0.03em;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
background: var(--brand-gradient);
|
background: var(--brand-gradient);
|
||||||
@@ -161,42 +167,37 @@ body {
|
|||||||
.brand__tagline {
|
.brand__tagline {
|
||||||
font-family: "Google Sans", sans-serif;
|
font-family: "Google Sans", sans-serif;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 11px;
|
font-size: 13px;
|
||||||
letter-spacing: 0.01em;
|
letter-spacing: 0.01em;
|
||||||
/* ensure legible light text on dark background (avoid fallback black) */
|
color: rgba(230, 224, 233, 0.9);
|
||||||
color: rgba(230,224,233,0.9);
|
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.install {
|
.install {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
/* move install row a bit lower to balance larger artboard */
|
/* Vertically centered in the gap between brand block (~151px) and notice (~292px). */
|
||||||
top: 170px;
|
top: 174px;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 18px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slot {
|
.slot {
|
||||||
width: var(--slot-size);
|
width: var(--slot-size);
|
||||||
height: var(--slot-size);
|
height: var(--slot-size);
|
||||||
border-radius: var(--slot-radius);
|
border: none;
|
||||||
/* subtle solid outline plus a gentle reduced-frequency dot pattern */
|
background: transparent;
|
||||||
border: 1px solid rgba(230,224,233,0.12);
|
|
||||||
background-color: transparent;
|
|
||||||
background-image: radial-gradient(rgba(255,255,255,0.03) 1px, transparent 1px);
|
|
||||||
background-size: 32px 32px;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow {
|
.arrow {
|
||||||
width: 36px;
|
width: 44px;
|
||||||
height: 36px;
|
height: 44px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
color: rgba(230, 224, 233, 0.88);
|
color: rgba(230, 224, 233, 0.88);
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -206,21 +207,20 @@ body {
|
|||||||
|
|
||||||
.arrow svg {
|
.arrow svg {
|
||||||
display: block;
|
display: block;
|
||||||
width: 36px;
|
width: 44px;
|
||||||
height: 36px;
|
height: 44px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notice {
|
.notice {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 20px;
|
left: 20px;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
/* increase bottom padding to avoid cropping */
|
bottom: calc(var(--window-chrome-bottom) + 16px);
|
||||||
bottom: 20px;
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-family: "Google Sans", sans-serif;
|
font-family: "Google Sans", sans-serif;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
color: var(--on-surface-muted);
|
color: var(--on-surface-muted);
|
||||||
text-wrap: balance;
|
text-wrap: balance;
|
||||||
@@ -231,39 +231,31 @@ body {
|
|||||||
color: var(--on-surface);
|
color: var(--on-surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Preview-only slot labels (white text). Finder will draw real labels; these are used for the exported preview. */
|
|
||||||
.slot__label {
|
.slot__label {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
margin-top: 12px;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
font-family: "Google Sans", system-ui, sans-serif;
|
font-family: "Google Sans", system-ui, sans-serif;
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
color: white; /* white app names as requested */
|
font-weight: 500;
|
||||||
|
line-height: 1.2;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
color: var(--primary-pill, #7e22ce);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: auto;
|
white-space: nowrap;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
box-sizing: border-box;
|
||||||
.slot__label[data-for="slot-app"],
|
min-height: 26px;
|
||||||
.slot__label[data-for="slot-applications"] {
|
padding: 5px 12px;
|
||||||
/* positioning is set dynamically by the export script */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* pill background behind label using ::before */
|
|
||||||
.slot__label {
|
|
||||||
position: absolute;
|
|
||||||
display: inline-block;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
.slot__label::before {
|
.slot__label::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
left: 0;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
height: 28px;
|
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
background: var(--primary-pill, #7e22ce);
|
background: var(--primary-pill, #7e22ce);
|
||||||
z-index: 2;
|
z-index: -1;
|
||||||
display: block;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+41
@@ -0,0 +1,41 @@
|
|||||||
|
# ProGuard specializes return types of @JvmMultifileClass actuals (VerifyError: Paragraph vs SkiaParagraph).
|
||||||
|
# Upstream: https://github.com/JetBrains/compose-multiplatform/pull/5652 (CMP-10488)
|
||||||
|
-keep,allowshrinking,allowobfuscation class **Kt__* { *; }
|
||||||
|
|
||||||
|
# Ktor ContentNegotiation JSON extension (ServiceLoader).
|
||||||
|
-keep class io.ktor.serialization.kotlinx.json.KotlinxSerializationJsonExtensionProvider { *; }
|
||||||
|
|
||||||
|
# Ktor CIO client engine (ServiceLoader).
|
||||||
|
-keep class io.ktor.client.engine.cio.CIOEngineContainer { *; }
|
||||||
|
|
||||||
|
# SQLite JDBC driver + JNI natives (ProGuard drops native methods otherwise).
|
||||||
|
-keepclasseswithmembernames class * {
|
||||||
|
native <methods>;
|
||||||
|
}
|
||||||
|
-keep class org.sqlite.** { *; }
|
||||||
|
|
||||||
|
# BouncyCastle security providers (ServiceLoader java.security.Provider).
|
||||||
|
-keep class org.bouncycastle.jce.provider.BouncyCastleProvider { *; }
|
||||||
|
-keep class org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider { *; }
|
||||||
|
|
||||||
|
# Cryptography provider (ServiceLoader).
|
||||||
|
-keep class dev.whyoleg.cryptography.providers.jdk.JdkCryptographyProviderContainer { *; }
|
||||||
|
|
||||||
|
# Coil fetchers/decoders registered via ServiceLoader (network + SVG).
|
||||||
|
-keep class coil3.network.ktor3.internal.KtorNetworkFetcherServiceLoaderTarget { *; }
|
||||||
|
-keep class coil3.svg.internal.SvgDecoderServiceLoaderTarget { *; }
|
||||||
|
|
||||||
|
# Compose Multiplatform generated resource accessors (logo, strings, fonts).
|
||||||
|
-keep class ru.fromchat.Drawable*_commonMainKt { *; }
|
||||||
|
-keep class ru.fromchat.String*_commonMainKt { *; }
|
||||||
|
-keep class ru.fromchat.Font*_commonMainKt { *; }
|
||||||
|
-keep class ru.fromchat.ActualResourceCollectorsKt { *; }
|
||||||
|
-keep class ru.fromchat.Res { *; }
|
||||||
|
-keep class ru.fromchat.Res$* { *; }
|
||||||
|
|
||||||
|
# App JSON models.
|
||||||
|
-keep @kotlinx.serialization.Serializable class ru.fromchat.** { *; }
|
||||||
|
-keepclassmembers @kotlinx.serialization.Serializable class ru.fromchat.** {
|
||||||
|
<fields>;
|
||||||
|
<init>(...);
|
||||||
|
}
|
||||||
@@ -71,6 +71,8 @@ kotlin {
|
|||||||
compileSdk = 37
|
compileSdk = 37
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jvmToolchain(17)
|
||||||
|
|
||||||
compilerOptions {
|
compilerOptions {
|
||||||
freeCompilerArgs.addAll("-Xexpect-actual-classes")
|
freeCompilerArgs.addAll("-Xexpect-actual-classes")
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 94 KiB |
@@ -1,5 +1,6 @@
|
|||||||
package ru.fromchat.ui
|
package ru.fromchat.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
@@ -26,13 +27,14 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coil3.compose.AsyncImage
|
import org.jetbrains.compose.resources.painterResource
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
import ru.fromchat.Res
|
import ru.fromchat.Res
|
||||||
import ru.fromchat.api.ApiClient
|
import ru.fromchat.api.ApiClient
|
||||||
import ru.fromchat.auth_get_started
|
import ru.fromchat.auth_get_started
|
||||||
import ru.fromchat.auth_welcome_tagline
|
import ru.fromchat.auth_welcome_tagline
|
||||||
import ru.fromchat.auth_welcome_title
|
import ru.fromchat.auth_welcome_title
|
||||||
|
import ru.fromchat.logo_square
|
||||||
import ru.fromchat.ui.auth.PreAuthOverflowMenu
|
import ru.fromchat.ui.auth.PreAuthOverflowMenu
|
||||||
import ru.fromchat.ui.components.ActionButton
|
import ru.fromchat.ui.components.ActionButton
|
||||||
import ru.fromchat.ui.components.Text
|
import ru.fromchat.ui.components.Text
|
||||||
@@ -82,8 +84,8 @@ fun WelcomeScreen(
|
|||||||
verticalArrangement = if (wide) Arrangement.Center else Arrangement.Center,
|
verticalArrangement = if (wide) Arrangement.Center else Arrangement.Center,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
AsyncImage(
|
Image(
|
||||||
model = Res.getUri("drawable/logo_square.png"),
|
painter = painterResource(Res.drawable.logo_square),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(112.dp)
|
.size(112.dp)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import androidx.compose.animation.fadeOut
|
|||||||
import androidx.compose.animation.slideInVertically
|
import androidx.compose.animation.slideInVertically
|
||||||
import androidx.compose.animation.slideOutVertically
|
import androidx.compose.animation.slideOutVertically
|
||||||
import androidx.compose.animation.togetherWith
|
import androidx.compose.animation.togetherWith
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -96,16 +97,17 @@ import androidx.compose.ui.unit.Dp
|
|||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.IntSize
|
import androidx.compose.ui.unit.IntSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coil3.compose.AsyncImage
|
|
||||||
import com.pr0gramm3r101.utils.supportClipboardManagerImpl
|
import com.pr0gramm3r101.utils.supportClipboardManagerImpl
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.jetbrains.compose.resources.painterResource
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
import ru.fromchat.Res
|
import ru.fromchat.Res
|
||||||
import ru.fromchat.action_delete
|
import ru.fromchat.action_delete
|
||||||
import ru.fromchat.action_mark_read
|
import ru.fromchat.action_mark_read
|
||||||
|
import ru.fromchat.logo_square
|
||||||
import ru.fromchat.api.ApiClient
|
import ru.fromchat.api.ApiClient
|
||||||
import ru.fromchat.api.ChatListSync
|
import ru.fromchat.api.ChatListSync
|
||||||
import ru.fromchat.api.StatusSubscriptionCoordinator
|
import ru.fromchat.api.StatusSubscriptionCoordinator
|
||||||
@@ -398,8 +400,8 @@ private fun ChatsNormalTopBar(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Start,
|
horizontalArrangement = Arrangement.Start,
|
||||||
) {
|
) {
|
||||||
AsyncImage(
|
Image(
|
||||||
model = Res.getUri("drawable/logo_square.png"),
|
painter = painterResource(Res.drawable.logo_square),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
contentScale = ContentScale.Fit,
|
contentScale = ContentScale.Fit,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package ru.fromchat.ui.main.settings
|
package ru.fromchat.ui.main.settings
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@@ -26,11 +27,11 @@ import androidx.compose.ui.platform.LocalUriHandler
|
|||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coil3.compose.AsyncImage
|
|
||||||
import com.pr0gramm3r101.components.Category
|
import com.pr0gramm3r101.components.Category
|
||||||
import com.pr0gramm3r101.components.ListItem
|
import com.pr0gramm3r101.components.ListItem
|
||||||
import com.pr0gramm3r101.ui.Website
|
import com.pr0gramm3r101.ui.Website
|
||||||
import com.pr0gramm3r101.utils.conditional
|
import com.pr0gramm3r101.utils.conditional
|
||||||
|
import org.jetbrains.compose.resources.painterResource
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
import org.jetbrains.compose.resources.vectorResource
|
import org.jetbrains.compose.resources.vectorResource
|
||||||
import ru.fromchat.AppBuildInfo
|
import ru.fromchat.AppBuildInfo
|
||||||
@@ -43,6 +44,7 @@ import ru.fromchat.about_link_terms
|
|||||||
import ru.fromchat.about_link_website
|
import ru.fromchat.about_link_website
|
||||||
import ru.fromchat.about_version
|
import ru.fromchat.about_version
|
||||||
import ru.fromchat.app_desc
|
import ru.fromchat.app_desc
|
||||||
|
import ru.fromchat.logo_square
|
||||||
import ru.fromchat.legal.DocumentType
|
import ru.fromchat.legal.DocumentType
|
||||||
import ru.fromchat.ui.LocalNavController
|
import ru.fromchat.ui.LocalNavController
|
||||||
import ru.fromchat.ui.components.BrandTitle
|
import ru.fromchat.ui.components.BrandTitle
|
||||||
@@ -91,13 +93,13 @@ fun AboutScreen() {
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Box(Modifier.padding(bottom = 12.dp)) {
|
Box(Modifier.padding(bottom = 12.dp)) {
|
||||||
AsyncImage(
|
Image(
|
||||||
model = Res.getUri("drawable/logo_square.png"),
|
painter = painterResource(Res.drawable.logo_square),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
contentScale = ContentScale.Fit,
|
contentScale = ContentScale.Fit,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(88.dp)
|
.size(88.dp)
|
||||||
.clip(RoundedCornerShape(28.dp))
|
.clip(RoundedCornerShape(28.dp)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import androidx.compose.animation.AnimatedVisibility
|
|||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
@@ -63,7 +64,9 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
|||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coil3.compose.AsyncImage
|
import org.jetbrains.compose.resources.DrawableResource
|
||||||
|
import org.jetbrains.compose.resources.painterResource
|
||||||
|
import org.jetbrains.compose.resources.stringResource
|
||||||
import com.pr0gramm3r101.components.Category
|
import com.pr0gramm3r101.components.Category
|
||||||
import com.pr0gramm3r101.components.ListItem
|
import com.pr0gramm3r101.components.ListItem
|
||||||
import com.pr0gramm3r101.utils.currentDeviceInfo
|
import com.pr0gramm3r101.utils.currentDeviceInfo
|
||||||
@@ -81,8 +84,10 @@ import kotlinx.coroutines.TimeoutCancellationException
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jetbrains.compose.resources.stringResource
|
|
||||||
import ru.fromchat.Res
|
import ru.fromchat.Res
|
||||||
|
import ru.fromchat.os_linux
|
||||||
|
import ru.fromchat.os_macos
|
||||||
|
import ru.fromchat.os_windows
|
||||||
import ru.fromchat.api.ApiClient
|
import ru.fromchat.api.ApiClient
|
||||||
import ru.fromchat.api.local.WebSocketManager
|
import ru.fromchat.api.local.WebSocketManager
|
||||||
import ru.fromchat.api.schema.user.devices.DeviceSessionInfo
|
import ru.fromchat.api.schema.user.devices.DeviceSessionInfo
|
||||||
@@ -238,11 +243,11 @@ private fun deviceSessionOsLine(d: DeviceSessionInfo): String? {
|
|||||||
return if (version.isBlank()) os else "$os $version"
|
return if (version.isBlank()) os else "$os $version"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun deviceSessionLogoResource(d: DeviceSessionInfo): String? {
|
private fun deviceSessionLogoResource(d: DeviceSessionInfo): DrawableResource? {
|
||||||
return when (resolveDeviceOsName(d)?.lowercase()) {
|
return when (resolveDeviceOsName(d)?.lowercase()) {
|
||||||
"windows", "windows nt" -> "drawable/os_windows.svg"
|
"windows", "windows nt" -> Res.drawable.os_windows
|
||||||
"mac", "mac os", "macos" -> "drawable/os_macos.svg"
|
"mac", "mac os", "macos" -> Res.drawable.os_macos
|
||||||
"linux" -> "drawable/os_linux.svg"
|
"linux" -> Res.drawable.os_linux
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -537,8 +542,8 @@ fun DevicesScreen(onBack: () -> Unit) {
|
|||||||
),
|
),
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
if (osLogo != null) {
|
if (osLogo != null) {
|
||||||
AsyncImage(
|
Image(
|
||||||
model = Res.getUri(osLogo),
|
painter = painterResource(osLogo),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(24.dp),
|
modifier = Modifier.size(24.dp),
|
||||||
contentScale = ContentScale.Fit,
|
contentScale = ContentScale.Fit,
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
|
jvmToolchain(17)
|
||||||
|
|
||||||
compilerOptions {
|
compilerOptions {
|
||||||
freeCompilerArgs.addAll("-Xexpect-actual-classes")
|
freeCompilerArgs.addAll("-Xexpect-actual-classes")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user